Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/fix-schema-input-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@aws-amplify/data-schema": patch
---

fix(schema): preserve `array`, `arrayRequired`, and `valueRequired` when generating input types for custom fields

Previously, nested custom input types dropped important metadata about array and requiredness.
This change ensures:

- `array` is passed through for referenced custom types
- `arrayRequired` and `valueRequired` are correctly preserved
- Inline custom types default to `false` for these fields

This fixes inconsistencies between schema definitions and generated GraphQL input types.
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ Return the schema definition as a graphql string, with amplify directives allowe

</td></tr>
</tbody></table>

Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,4 @@ _(Optional)_ If an error can be associated to a particular field in the GraphQL

</td></tr>
</tbody></table>

1 change: 1 addition & 0 deletions packages/data-schema-types/docs/data-schema-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,3 +563,4 @@ Replaces the value of a key in a complex generic type param

</td></tr>
</tbody></table>

Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ number

</td></tr>
</tbody></table>

1 change: 1 addition & 0 deletions packages/data-schema-types/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ Description

</td></tr>
</tbody></table>

90 changes: 90 additions & 0 deletions packages/data-schema/__tests__/CustomOperations.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,3 +1051,93 @@ describe('.arguments() modifier', () => {
type Test = Expect<Equal<ActualArgs, ExpectedArgs>>;
});
});

describe('input type metadata preservation - TypeScript types', () => {
it('should handle TodoUpsert schema with array and required modifiers', () => {
const schema = a.schema({
TodoTagType: a.customType({
name: a.string().required(),
color: a.string().required(),
}),
Todo: a
.model({
content: a.string(),
tags: a.ref('TodoTagType').array(),
updatedTs: a.integer(),
}),
TodoUpsert: a.customType({
content: a.string(),
tags: a.ref('TodoTagType').array(),
updatedTs: a.integer(),
}),
batchUpsertTodos: a
.mutation()
.arguments({
tableName: a.string().required(),
items: a.ref('TodoUpsert').array().required(),
})
.returns(a.ref('Todo').array())
.handler(a.handler.function('myFunc')),
});

type Schema = ClientSchema<typeof schema>;

// Verify that the type structure includes required fields properly
type ActualArgs = Schema['batchUpsertTodos']['args'];

// Basic type structure verification - the exact shape is verified in runtime tests
const _typeCheck: ActualArgs = {} as ActualArgs;

// These assignments should work with your fix
Copy link
Member

@ahmedhamouda78 ahmedhamouda78 Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing this comment was generated by an LLM. Can you please review/remove all such comments?

const validArgs: ActualArgs = {
tableName: 'test',
items: [{
content: 'test content',
tags: [{ name: 'tag1', color: 'blue' }],
updatedTs: 12345
}]
};

// This should be a type compile-time validation that the types work correctly
expect(validArgs).toBeDefined();
});

it('should preserve array and required modifiers in TypeScript types', () => {
const schema = a.schema({
TagType: a.customType({
name: a.string().required(),
color: a.string().required(),
}),
testMutation: a
.mutation()
.arguments({
requiredTags: a.ref('TagType').array().required(),
optionalTags: a.ref('TagType').array(),
singleRequiredTag: a.ref('TagType').required(),
singleOptionalTag: a.ref('TagType'),
})
.returns(a.string())
.handler(a.handler.function('myFunc')),
});

type Schema = ClientSchema<typeof schema>;
type ActualArgs = Schema['testMutation']['args'];

// Verify the structure allows valid assignments
const validArgs: ActualArgs = {
requiredTags: [{ name: 'tag1', color: 'red' }],
singleRequiredTag: { name: 'tag2', color: 'blue' },
// optionalTags and singleOptionalTag can be omitted
};

const validArgsWithOptionals: ActualArgs = {
requiredTags: [{ name: 'tag1', color: 'red' }],
singleRequiredTag: { name: 'tag2', color: 'blue' },
optionalTags: [{ name: 'tag3', color: 'green' }],
singleOptionalTag: { name: 'tag4', color: 'yellow' },
};

expect(validArgs).toBeDefined();
expect(validArgsWithOptionals).toBeDefined();
});
});
226 changes: 226 additions & 0 deletions packages/data-schema/__tests__/CustomOperations.test.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding tests! Can you also add test coverage to the ModelSchema.test.ts file as it is more focused on testing custom operations argument inputs? You can refer to PR #617 for these tests

Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,232 @@ describe('CustomOperation transform', () => {
});
});

describe('input type metadata preservation', () => {
test('regression: existing simple input types still work correctly', () => {
const s = a
.schema({
post: a.customType({
title: a.string(),
content: a.string(),
}),
simpleQuery: a
.query()
.arguments({
simpleArg: a.ref('post'),
})
.returns(a.string())
.handler(a.handler.function('myFunc')),
})
.authorization((allow) => allow.publicApiKey());

const result = s.transform().schema;

// Verify the input type is generated correctly for simple cases
expect(result).toContain('input postInput');
expect(result).toContain('simpleArg: postInput');

expect(result).toMatchSnapshot();
});

test('should preserve array and required modifiers in input types', () => {
// Based on the user's real schema that demonstrates the bug
const s = a
.schema({
TodoTagType: a.customType({
name: a.string().required(),
color: a.string().required(),
}),
Todo: a
.model({
content: a.string(),
tags: a.ref('TodoTagType').array(),
updatedTs: a.integer(),
}),
TodoUpsert: a.customType({
content: a.string(),
tags: a.ref('TodoTagType').array(),
updatedTs: a.integer(),
}),
batchUpsertTodos: a
.mutation()
.arguments({
tableName: a.string().required(),
items: a.ref('TodoUpsert').array().required(),
})
.returns(a.ref('Todo').array())
.handler(a.handler.function('myFunc')),
})
.authorization((allow) => allow.publicApiKey());

const result = s.transform().schema;

// Verify the input types are generated with proper GraphQL syntax
expect(result).toContain('input TodoTagTypeInput');
expect(result).toContain('input TodoUpsertInput');

// The key test: verify that array and required modifiers are preserved
expect(result).toContain('tableName: String!');
expect(result).toContain('items: [TodoUpsertInput]!'); // Fixed: array is required but elements are not

// Verify nested structure is correct too
expect(result).toContain('tags: [TodoTagTypeInput]');

expect(result).toMatchSnapshot();
});

test('preserves array().required() modifier on referenced custom type arguments', () => {
const s = a
.schema({
TagType: a.customType({
name: a.string().required(),
color: a.string().required(),
}),
testMutation: a
.mutation()
.arguments({
requiredTags: a.ref('TagType').array().required(), // Array is required, items are optional
optionalTags: a.ref('TagType').array(),
singleRequiredTag: a.ref('TagType').required(),
singleOptionalTag: a.ref('TagType'),
bothRequired: a.ref('TagType').required().array().required(), // Both array and items required
})
.returns(a.string())
.handler(a.handler.function('myFunc')),
})
.authorization((allow) => allow.publicApiKey());

const result = s.transform().schema;

// Verify the input type is generated with proper GraphQL syntax
expect(result).toContain('input TagTypeInput');
expect(result).toContain('requiredTags: [TagTypeInput]!'); // Array required, items optional
expect(result).toContain('optionalTags: [TagTypeInput]'); // Both optional
expect(result).toContain('singleRequiredTag: TagTypeInput!'); // Single item required
expect(result).toContain('singleOptionalTag: TagTypeInput'); // Single item optional
expect(result).toContain('bothRequired: [TagTypeInput!]!'); // Both required

expect(result).toMatchSnapshot();
});

test('preserves modifiers on nested custom type references', () => {
const s = a
.schema({
InnerType: a.customType({
value: a.string(),
}),
OuterType: a.customType({
innerItems: a.ref('InnerType').array(),
singleInner: a.ref('InnerType').required(),
}),
testMutation: a
.mutation()
.arguments({
data: a.ref('OuterType').required(),
dataArray: a.ref('OuterType').array().required(),
})
.returns(a.string())
.handler(a.handler.function('myFunc')),
})
.authorization((allow) => allow.publicApiKey());

const result = s.transform().schema;

// Verify all input types are generated
expect(result).toContain('input InnerTypeInput');
expect(result).toContain('input OuterTypeInput');

// Verify top-level argument modifiers are preserved
expect(result).toContain('data: OuterTypeInput!');
expect(result).toContain('dataArray: [OuterTypeInput]!'); // Array required, items optional

// Verify nested field modifiers are preserved
expect(result).toContain('innerItems: [InnerTypeInput]');
expect(result).toContain('singleInner: InnerTypeInput!');

expect(result).toMatchSnapshot();
});

test('preserves modifiers in complex batch operation scenarios', () => {
const s = a
.schema({
MetadataType: a.customType({
version: a.string().required(),
timestamp: a.integer().required(),
}),
ItemType: a.customType({
id: a.string().required(),
data: a.string(),
metadata: a.ref('MetadataType').required(),
tags: a.ref('MetadataType').array(),
}),
batchOperation: a
.mutation()
.arguments({
operationId: a.string().required(),
itemsToCreate: a.ref('ItemType').array().required(),
itemsToUpdate: a.ref('ItemType').array(),
metadataOverride: a.ref('MetadataType'),
})
.returns(a.string())
.handler(a.handler.function('myFunc')),
})
.authorization((allow) => allow.publicApiKey());

const result = s.transform().schema;

// Verify all input types are generated
expect(result).toContain('input MetadataTypeInput');
expect(result).toContain('input ItemTypeInput');

// Verify argument modifiers are preserved
expect(result).toContain('operationId: String!');
expect(result).toContain('itemsToCreate: [ItemTypeInput]!'); // Array required, items optional
expect(result).toContain('itemsToUpdate: [ItemTypeInput]');
expect(result).toContain('metadataOverride: MetadataTypeInput');

// Verify nested field modifiers are preserved
expect(result).toContain('metadata: MetadataTypeInput!');
expect(result).toContain('tags: [MetadataTypeInput]');

expect(result).toMatchSnapshot();
});

test('preserves modifiers in subscription arguments', () => {
const s = a
.schema({
FilterType: a.customType({
category: a.string().required(),
priority: a.integer(),
}),
someMutation: a
.mutation()
.arguments({ input: a.string() })
.returns(a.string())
.handler(a.handler.function('myFunc')),
subscribeToUpdates: a
.subscription()
.arguments({
filters: a.ref('FilterType').array().required(),
globalFilter: a.ref('FilterType'),
})
.handler(a.handler.function('myFunc'))
.for(a.ref('someMutation')),
})
.authorization((allow) => allow.publicApiKey());

const result = s.transform().schema;

// Verify input types are generated
expect(result).toContain('input FilterTypeInput');

// Verify subscription argument modifiers are preserved
expect(result).toContain('filters: [FilterTypeInput]!'); // Array required, items optional
expect(result).toContain('globalFilter: FilterTypeInput');

expect(result).toMatchSnapshot();
});
});

const fakeSecret = () => ({}) as any;

const datasourceConfigMySQL = {
Expand Down
Loading
Loading