From e7f97fa103befa9fc1ce8b94c01d1b55bc8f9bdf Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Thu, 7 Nov 2024 17:20:23 -0800 Subject: [PATCH 01/12] feat(custom-operations): allow CustomType and RefType in arguments --- .changeset/polite-crabs-rush.md | 5 ++++ .../__tests__/CustomOperations.test-d.ts | 25 +++++++++++++++++++ packages/data-schema/src/CustomOperation.ts | 2 +- .../data-schema/src/runtime/bridge-types.ts | 6 +++-- 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 .changeset/polite-crabs-rush.md diff --git a/.changeset/polite-crabs-rush.md b/.changeset/polite-crabs-rush.md new file mode 100644 index 000000000..dd96cee72 --- /dev/null +++ b/.changeset/polite-crabs-rush.md @@ -0,0 +1,5 @@ +--- +"@aws-amplify/data-schema": minor +--- + +Allow CustomType and RefType in arguments for custom operations diff --git a/packages/data-schema/__tests__/CustomOperations.test-d.ts b/packages/data-schema/__tests__/CustomOperations.test-d.ts index bac164008..065c30515 100644 --- a/packages/data-schema/__tests__/CustomOperations.test-d.ts +++ b/packages/data-schema/__tests__/CustomOperations.test-d.ts @@ -16,6 +16,10 @@ import type { import { configure } from '../src/ModelSchema'; import { Nullable } from '../src/ModelField'; import { defineFunctionStub } from './utils'; +import type { + CustomOperation, +} from '../src/CustomOperation'; +import { expectType } from 'tsd'; describe('custom operations return types', () => { describe('when .ref() a basic custom type', () => { @@ -858,3 +862,24 @@ describe('.for() modifier', () => { a.subscription().for(a.ref('Model')); }); }); + +describe('.arguments() modifier', () => { + // Test to verify that CustomType can be used as an argument in custom operations + it('accepts CustomType in arguments', () => { + const operation = a.query().arguments({ + customArg: a.customType({ + field1: a.string(), + field2: a.integer() + }) + }); + expectType>(operation); + }); + + // Test to verify that RefType can be used as an argument in custom operations + it('accepts RefType in arguments', () => { + const operation = a.query().arguments({ + refArg: a.ref('SomeType') + }); + expectType>(operation); + }); +}); \ No newline at end of file diff --git a/packages/data-schema/src/CustomOperation.ts b/packages/data-schema/src/CustomOperation.ts index db201e3fa..43257b688 100644 --- a/packages/data-schema/src/CustomOperation.ts +++ b/packages/data-schema/src/CustomOperation.ts @@ -29,7 +29,7 @@ type CustomOperationBrand = | typeof subscriptionBrand | typeof generationBrand; -type CustomArguments = Record; +type CustomArguments = Record | RefType>; type SubscriptionSource = RefType; type InternalSubscriptionSource = InternalRef; type CustomReturnType = RefType | CustomType; diff --git a/packages/data-schema/src/runtime/bridge-types.ts b/packages/data-schema/src/runtime/bridge-types.ts index 93185146c..ffe73e22e 100644 --- a/packages/data-schema/src/runtime/bridge-types.ts +++ b/packages/data-schema/src/runtime/bridge-types.ts @@ -16,6 +16,8 @@ import { Observable } from 'rxjs'; import { CustomHeaders, ModelSortDirection } from './client'; import { AiAction, AiCategory } from './internals/ai/getCustomUserAgentDetails'; +import { CustomType } from '../CustomType'; +import { RefType } from '../RefType'; export declare namespace AmplifyServer { export interface ContextToken { @@ -165,7 +167,7 @@ export type CustomOperationArguments = Record; export interface CustomOperationArgument { name: string; - type: InputFieldType; + type: InputFieldType | CustomType | RefType; isArray: boolean; isRequired: boolean; isArrayNullable?: boolean; @@ -240,7 +242,7 @@ export type FieldType = | ModelFieldType | NonModelFieldType; -export type InputFieldType = ScalarType | EnumType | InputType; +export type InputFieldType = ScalarType | EnumType | InputType | CustomType | RefType; export type FieldAttribute = ModelAttribute; From 0e0561e6aa7901d1786d8751eb35b51b1e6517d7 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Thu, 7 Nov 2024 23:46:52 -0800 Subject: [PATCH 02/12] Refactor type tests for CustomType and RefType --- packages/data-schema/__tests__/CustomOperations.test-d.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/data-schema/__tests__/CustomOperations.test-d.ts b/packages/data-schema/__tests__/CustomOperations.test-d.ts index 065c30515..65bbd936e 100644 --- a/packages/data-schema/__tests__/CustomOperations.test-d.ts +++ b/packages/data-schema/__tests__/CustomOperations.test-d.ts @@ -19,7 +19,6 @@ import { defineFunctionStub } from './utils'; import type { CustomOperation, } from '../src/CustomOperation'; -import { expectType } from 'tsd'; describe('custom operations return types', () => { describe('when .ref() a basic custom type', () => { @@ -866,20 +865,18 @@ describe('.for() modifier', () => { describe('.arguments() modifier', () => { // Test to verify that CustomType can be used as an argument in custom operations it('accepts CustomType in arguments', () => { - const operation = a.query().arguments({ + const operation: CustomOperation = a.query().arguments({ customArg: a.customType({ field1: a.string(), field2: a.integer() }) }); - expectType>(operation); }); // Test to verify that RefType can be used as an argument in custom operations it('accepts RefType in arguments', () => { - const operation = a.query().arguments({ + const operation:CustomOperation = a.query().arguments({ refArg: a.ref('SomeType') }); - expectType>(operation); }); }); \ No newline at end of file From a0872b99135a1be1658fad666d1122fe225535ce Mon Sep 17 00:00:00 2001 From: Vaisshnavi Voddnaalaa <78839923+vaisshnavi7@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:19:49 -0800 Subject: [PATCH 03/12] feat: add support for custom, nested, and ref type arguments (#402) Co-authored-by: aws-amplify-bot --- .../Core/ClientCustomOperations.ts | 29 ++- .../2-expected-use/custom-operations.ts | 181 +++++++++++++++++- 2 files changed, 185 insertions(+), 25 deletions(-) diff --git a/packages/data-schema/src/ClientSchema/Core/ClientCustomOperations.ts b/packages/data-schema/src/ClientSchema/Core/ClientCustomOperations.ts index 6f7d3dfac..f96d87e18 100644 --- a/packages/data-schema/src/ClientSchema/Core/ClientCustomOperations.ts +++ b/packages/data-schema/src/ClientSchema/Core/ClientCustomOperations.ts @@ -9,8 +9,8 @@ import type { AppSyncResolverHandler } from 'aws-lambda'; import type { CustomType } from '../../CustomType'; import type { FieldTypesOfCustomType } from '../../MappedTypes/ResolveSchema'; import type { ResolveRef } from '../utilities/ResolveRef'; -import type { EnumType } from '../../EnumType'; import { ClientSchemaProperty } from './ClientSchemaProperty'; +import type { ResolveFields } from '../utilities'; type CustomOperationSubType = `custom${Op['typeName']}`; @@ -38,7 +38,7 @@ export interface ClientCustomOperation< * ``` */ functionHandler: AppSyncResolverHandler< - CustomOpArguments, + CustomOpArguments, // If the final handler is an async function, the Schema['fieldname']['functionhandler'] // should have a return type of `void`. This only applies to `functionHandler` and not // `returnType` because `returnType` determines the type returned by the mutation / query @@ -60,7 +60,7 @@ export interface ClientCustomOperation< * } * ``` */ - args: CustomOpArguments; + args: CustomOpArguments; /** * The return type expected by a lambda function handler. @@ -84,19 +84,18 @@ export interface ClientCustomOperation< /** * Digs out custom operation arguments, mapped to the intended graphql types. + * using the existing ResolveFields utility type. This handles: + * - Basic scalar fields + * - Enum types + * - Custom types (including nested structures) + * - Reference types */ -type CustomOpArguments = - Shape['arguments'] extends null - ? never - : ResolveFieldRequirements<{ - [FieldName in keyof Shape['arguments']]: Shape['arguments'][FieldName] extends BaseModelField< - infer R - > - ? R - : Shape['arguments'][FieldName] extends EnumType - ? Values[number] | null - : never; - }>; +type CustomOpArguments< + Shape extends CustomOperationParamShape, + RefBag extends Record = any, +> = Shape['arguments'] extends null + ? never + : ResolveFields; /** * Removes `null | undefined` from the return type if the operation is a subscription, diff --git a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts index d23ec3cf4..2f9bced61 100644 --- a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts +++ b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts @@ -123,13 +123,174 @@ describe('custom operations', () => { a.handler.function(dummyHandler).async(), ]) .authorization((allow) => [allow.publicApiKey()]), + queryWithCustomTypeArg: a + .query() + .arguments({ + customArg: a.customType({ + message: a.string(), + count: a.integer(), + }), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), + mutateWithCustomTypeArg: a + .mutation() + .arguments({ + customArg: a.customType({ + message: a.string(), + count: a.integer(), + }), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), + mutationWithNestedCustomType: a + .mutation() + .arguments({ + nestedField: a.customType({ + nestedObject1: a.customType({ + innerField1: a.boolean(), + innerField2: a.string(), + }), + }), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), + queryWithRefArg: a + .query() + .arguments({ + refArg: a.ref('EchoResult'), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), + mutationWithRefArg: a + .mutation() + .arguments({ + refArg: a.ref('EchoResult'), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), + complexQueryOperation: a + .query() + .arguments({ + scalarArg: a.string(), + customArg: a.customType({ + field1: a.string(), + field2: a.integer(), + }), + refArg: a.ref('EchoResult'), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), + complexMutation: a + .mutation() + .arguments({ + scalarArg: a.string(), + customArg: a.customType({ + field1: a.string(), + field2: a.integer(), + }), + refArg: a.ref('EchoResult'), + }) + .returns(a.string()) + .handler(a.handler.function(dummyHandler)) + .authorization((allow) => [allow.publicApiKey()]), }); type Schema = ClientSchema; + type ExpectedQueryWithCustomTypeArg = { + customArg?: { + message?: string | null; + count?: number | null; + } | null; + }; + type ActualQuertWithCustomTypeArg = Schema['queryWithCustomTypeArg']['args']; + type TestEchoWithCustomTypeArg = Expect< + Equal + >; + + type ExpectedMutateWithCustomTypeArg = { + customArg?: { + message?: string | null; + count?: number | null; + } | null; + }; + type ActualMutateWithCustomTypeArg = + Schema['mutateWithCustomTypeArg']['args']; + type TestMutateWithCustomTypeArg = Expect< + Equal + >; + + type ExpectedNestedCustomTypeArgs = { + nestedField?: { + nestedObject1?: { + innerField1?: boolean | null; + innerField2?: string | null; + } | null; + } | null; + }; + type ActualNestedCustomTypeArgs = + Schema['mutationWithNestedCustomType']['args']; + type TestNestedCustomTypeArgs = Expect< + Equal + >; + + type ExpectedQueryWithRefArg = { + refArg?: { + result?: string | null; + } | null; + }; + type ActualQueryWithRefArg = Schema['queryWithRefArg']['args']; + type TestQueryWithRefArg = Expect< + Equal + >; + + type ExpectedMutationWithRefArg = { + refArg?: { + result?: string | null; + } | null; + }; + type ActualMutationWithRefArg = Schema['mutationWithRefArg']['args']; + type TestMutationWithRefArg = Expect< + Equal + >; + + type ExpectedComplexArgs = { + scalarArg?: string | null; + customArg?: { + field1?: string | null; + field2?: number | null; + } | null; + refArg?: { + result?: string | null; + } | null; + }; + type ActualComplexArgs = Schema['complexQueryOperation']['args']; + type TestComplexArgs = Expect>; + + type ExpectedComplexMutationArgs = { + scalarArg?: string | null; + customArg?: { + field1?: string | null; + field2?: number | null; + } | null; + refArg?: { + result?: string | null; + } | null; + }; + type ActualComplexMutationArgs = Schema['complexMutation']['args']; + type TestComplexMutationArgs = Expect< + Equal + >; // #endregion - test('primitive type result', async () => { + test.skip('primitive type result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -150,7 +311,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('custom type result', async () => { + test.skip('custom type result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -175,7 +336,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('custom type array result', async () => { + test.skip('custom type array result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -214,7 +375,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('model result', async () => { + test.skip('model result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -246,7 +407,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('model array result', async () => { + test.skip('model array result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -298,7 +459,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('solo async handler', async () => { + test.skip('solo async handler', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -326,7 +487,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('async sync', async () => { + test.skip('async sync', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -352,7 +513,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('sync sync', async () => { + test.skip('sync sync', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -378,7 +539,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('sync async', async () => { + test.skip('sync async', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -406,7 +567,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test('async async', async () => { + test.skip('async async', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { From 351764300f576a61e863b8ed4d6d54aa0b765e3c Mon Sep 17 00:00:00 2001 From: Vaisshnavi Voddnaalaa <78839923+vaisshnavi7@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:27:38 -0800 Subject: [PATCH 04/12] feat: Custom type and ref arg handling in SchemaProcessor with tests (#412) --- .../__tests__/CustomOperations.test.ts | 80 +++-- .../__snapshots__/ClientSchema.test.ts.snap | 26 +- .../CustomOperations.test.ts.snap | 28 +- .../__snapshots__/CustomType.test.ts.snap | 96 +++--- .../__snapshots__/EnumType.test.ts.snap | 48 +-- .../__snapshots__/ModelIndex.test.ts.snap | 50 ++-- packages/data-schema/src/SchemaProcessor.ts | 277 +++++++++++++++--- .../__snapshots__/client-schema.ts.snap | 14 +- .../__snapshots__/custom-operations.ts.snap | 80 +++++ .../2-expected-use/custom-operations.ts | 179 ++++++++--- 10 files changed, 643 insertions(+), 235 deletions(-) diff --git a/packages/data-schema/__tests__/CustomOperations.test.ts b/packages/data-schema/__tests__/CustomOperations.test.ts index dc6bd7b96..a841ed336 100644 --- a/packages/data-schema/__tests__/CustomOperations.test.ts +++ b/packages/data-schema/__tests__/CustomOperations.test.ts @@ -907,7 +907,7 @@ describe('CustomOperation transform', () => { .query() .arguments({}) .handler(a.handler.function(fn1).async()) - .authorization((allow) => allow.authenticated()) + .authorization((allow) => allow.authenticated()), }); const { schema, lambdaFunctions } = s.transform(); @@ -915,7 +915,7 @@ describe('CustomOperation transform', () => { expect(lambdaFunctions).toMatchObject({ FnGetPostDetails: fn1, }); - }) + }); test('defineFunction sync - async', () => { const fn1 = defineFunctionStub({}); @@ -927,7 +927,7 @@ describe('CustomOperation transform', () => { a.handler.function(fn1), a.handler.function(fn1).async(), ]) - .authorization((allow) => allow.authenticated()) + .authorization((allow) => allow.authenticated()), }); const { schema, lambdaFunctions } = s.transform(); @@ -935,7 +935,7 @@ describe('CustomOperation transform', () => { expect(lambdaFunctions).toMatchObject({ FnGetPostDetails: fn1, }); - }) + }); test('defineFunction sync - async with returns generates type errors', () => { const fn1 = defineFunctionStub({}); @@ -949,9 +949,9 @@ describe('CustomOperation transform', () => { ]) .authorization((allow) => allow.authenticated()) // @ts-expect-error - .returns({ }) + .returns({}), }); - }) + }); test('defineFunction async - async', () => { const fn1 = defineFunctionStub({}); @@ -965,7 +965,7 @@ describe('CustomOperation transform', () => { a.handler.function(fn1).async(), a.handler.function(fn2).async(), ]) - .authorization((allow) => allow.authenticated()) + .authorization((allow) => allow.authenticated()), }); const { schema, lambdaFunctions } = s.transform(); @@ -974,7 +974,7 @@ describe('CustomOperation transform', () => { FnGetPostDetails: fn1, FnGetPostDetails2: fn2, }); - }) + }); test('defineFunction async - sync', () => { const fn1 = defineFunctionStub({}); @@ -987,12 +987,12 @@ describe('CustomOperation transform', () => { a.handler.function(fn1), ]) .returns(a.customType({})) - .authorization((allow) => allow.authenticated()) + .authorization((allow) => allow.authenticated()), }); const { schema, lambdaFunctions } = s.transform(); expect(schema).toMatchSnapshot(); - }) + }); test('pipeline / mix', () => { const fn1 = defineFunctionStub({}); @@ -1341,15 +1341,14 @@ describe('custom operations + custom type auth inheritance', () => { test('implicit custom type inherits auth rules from referencing op', () => { const s = a.schema({ + MyQueryReturnType: a.customType({ + fieldA: a.string(), + fieldB: a.integer(), + }), myQuery: a .query() .handler(a.handler.function('myFn')) - .returns( - a.customType({ - fieldA: a.string(), - fieldB: a.integer(), - }), - ) + .returns(a.ref('MyQueryReturnType')) .authorization((allow) => allow.publicApiKey()), }); @@ -1363,23 +1362,22 @@ describe('custom operations + custom type auth inheritance', () => { test('nested custom types inherit auth rules from top-level referencing op', () => { const s = a.schema({ + MyQueryReturnType: a.customType({ + fieldA: a.string(), + fieldB: a.integer(), + nestedCustomType: a.customType({ + nestedA: a.string(), + nestedB: a.string(), + grandChild: a.customType({ + grandA: a.string(), + grandB: a.string(), + }), + }), + }), myQuery: a .query() .handler(a.handler.function('myFn')) - .returns( - a.customType({ - fieldA: a.string(), - fieldB: a.integer(), - nestedCustomType: a.customType({ - nestedA: a.string(), - nestedB: a.string(), - grandChild: a.customType({ - grandA: a.string(), - grandB: a.string(), - }), - }), - }), - ) + .returns(a.ref('MyQueryReturnType')) .authorization((allow) => allow.publicApiKey()), }); @@ -1401,6 +1399,28 @@ describe('custom operations + custom type auth inheritance', () => { ); }); + test('inline custom type inherits auth rules from referencing op', () => { + const s = a.schema({ + myQuery: a + .query() + .handler(a.handler.function('myFn')) + .returns( + a.customType({ + fieldA: a.string(), + fieldB: a.integer(), + }), + ) + .authorization((allow) => allow.publicApiKey()), + }); + + const result = s.transform().schema; + + expect(result).toMatchSnapshot(); + expect(result).toEqual( + expect.stringContaining('type MyQueryReturnType @aws_api_key\n{'), + ); + }); + test('top-level custom type with nested top-level custom types inherits combined auth rules from referencing ops', () => { const s = a.schema({ myQuery: a diff --git a/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap index 059b9149b..8f6bb1318 100644 --- a/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap @@ -242,20 +242,20 @@ type Query { `; exports[`custom operations Add entities to SQL schema add custom type, enum, and custom query to generated SQL schema 1`] = ` -"type post @model(timestamps: null) @auth(rules: [{allow: private}]) -{ - title: String! - description: String - author: String -} - -enum PostStatus { +"enum PostStatus { draft pending approved published } +type post @model(timestamps: null) @auth(rules: [{allow: private}]) +{ + title: String! + description: String + author: String +} + type PostMeta @aws_cognito_user_pools { viewCount: Int @@ -595,7 +595,11 @@ exports[`schema auth rules global public auth - multiple models 1`] = ` "functionSlots": [], "jsFunctions": [], "lambdaFunctions": {}, - "schema": "type A @model @auth(rules: [{allow: public, provider: apiKey}]) + "schema": "enum DTired { + ? +} + +type A @model @auth(rules: [{allow: public, provider: apiKey}]) { field: String } @@ -621,10 +625,6 @@ type D @model @auth(rules: [{allow: public, provider: apiKey}]) tired: DTired cId: ID c: C @belongsTo(references: ["cId"]) -} - -enum DTired { - ? }", } `; diff --git a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap index 98d9201fc..a81507f10 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap @@ -1,16 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CustomOperation transform dynamo schema Custom Mutation w required arg and enum 1`] = ` -"type Post @model @auth(rules: [{allow: private}]) -{ - title: String -} - -enum LikePostReactionType { +"enum LikePostReactionType { :shipit: :risitas: } +type Post @model @auth(rules: [{allow: private}]) +{ + title: String +} + type Mutation { likePost(postId: String!, reactionType: LikePostReactionType): Post }" @@ -45,7 +45,7 @@ exports[`CustomOperation transform dynamo schema Custom mutation w inline boolea `; exports[`CustomOperation transform dynamo schema Custom mutation w inline custom return type 1`] = ` -"type LikePostReturnType +"type LikePostReturnType { stringField: String intField: Int @@ -109,7 +109,7 @@ type Query { `; exports[`CustomOperation transform dynamo schema Custom query w inline custom return type 1`] = ` -"type GetPostDetailsReturnType +"type GetPostDetailsReturnType { stringField: String intField: Int @@ -545,6 +545,18 @@ type Query { }" `; +exports[`custom operations + custom type auth inheritance inline custom type inherits auth rules from referencing op 1`] = ` +"type MyQueryReturnType @aws_api_key +{ + fieldA: String + fieldB: Int +} + +type Query { + myQuery: MyQueryReturnType @function(name: "myFn") @auth(rules: [{allow: public, provider: apiKey}]) +}" +`; + exports[`custom operations + custom type auth inheritance nested custom types inherit auth rules from top-level referencing op 1`] = ` "type MyQueryReturnType @aws_api_key { diff --git a/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap index 3b1a10820..f7b601e28 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap @@ -40,16 +40,16 @@ type Location `; exports[`CustomType transform Explicit CustomType nests explicit CustomType 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - meta: Meta -} - -enum PostStatus { +"enum PostStatus { unpublished published } +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + meta: Meta +} + type Meta { status: PostStatus @@ -63,16 +63,16 @@ type AltMeta `; exports[`CustomType transform Explicit CustomType nests explicit enum 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - meta: Meta -} - -enum PostStatus { +"enum PostStatus { unpublished published } +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + meta: Meta +} + type Meta { status: PostStatus @@ -81,7 +81,12 @@ type Meta `; exports[`CustomType transform Explicit CustomType nests implicit CustomType 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum MetaStatus { + unpublished + published +} + +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: Meta } @@ -92,11 +97,6 @@ type Meta nestedMeta: MetaNestedMeta } -enum MetaStatus { - unpublished - published -} - type MetaNestedMeta { field1: String @@ -104,7 +104,12 @@ type MetaNestedMeta `; exports[`CustomType transform Explicit CustomType nests implicit enum type 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum MetaStatus { + unpublished + published +} + +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: Meta } @@ -113,11 +118,6 @@ type Meta { status: MetaStatus publishedDate: AWSDate -} - -enum MetaStatus { - unpublished - published }" `; @@ -135,7 +135,12 @@ type PostLocation `; exports[`CustomType transform Implicit CustomType nests explicit CustomType 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum PostMetaStatus { + unpublished + published +} + +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -149,25 +154,20 @@ type PostMeta { status: PostMetaStatus nestedMeta: AltMeta -} - -enum PostMetaStatus { - unpublished - published }" `; exports[`CustomType transform Implicit CustomType nests explicit enum 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - meta: PostMeta -} - -enum PostStatus { +"enum PostStatus { unpublished published } +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + meta: PostMeta +} + type PostMeta { status: PostStatus @@ -176,7 +176,12 @@ type PostMeta `; exports[`CustomType transform Implicit CustomType nests implicit CustomType 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum PostMetaStatus { + unpublished + published +} + +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -187,11 +192,6 @@ type PostMeta nestedMeta: PostMetaNestedMeta } -enum PostMetaStatus { - unpublished - published -} - type PostMetaNestedMeta { field1: String @@ -199,7 +199,12 @@ type PostMetaNestedMeta `; exports[`CustomType transform Implicit CustomType nests implicit enum type 1`] = ` -"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum PostMetaStatus { + unpublished + published +} + +type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -208,10 +213,5 @@ type PostMeta { status: PostMetaStatus publishedDate: AWSDate -} - -enum PostMetaStatus { - unpublished - published }" `; diff --git a/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap index 524a00326..2492d578d 100644 --- a/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap @@ -1,53 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EnumType transform Explicit Enum - auth 1`] = ` -"type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: AccessLevel @auth(rules: [{allow: owner, ownerField: "owner"}]) -} - -enum AccessLevel { +"enum AccessLevel { public protected private +} + +type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: AccessLevel @auth(rules: [{allow: owner, ownerField: "owner"}]) }" `; exports[`EnumType transform Explicit Enum - required 1`] = ` -"type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: AccessLevel! -} - -enum AccessLevel { +"enum AccessLevel { public protected private +} + +type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: AccessLevel! }" `; exports[`EnumType transform Explicit Enum 1`] = ` -"type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: AccessLevel -} - -enum AccessLevel { +"enum AccessLevel { public protected private +} + +type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: AccessLevel }" `; exports[`EnumType transform Implicit Enum 1`] = ` -"type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: FileAccessLevel -} - -enum FileAccessLevel { +"enum FileAccessLevel { public protected private +} + +type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: FileAccessLevel }" `; diff --git a/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap index 913272736..112cf923f 100644 --- a/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap @@ -1,61 +1,61 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`secondary index schema generation generates correct schema for using a.enum() as the partition key 1`] = ` -"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum TodoStatus { + open + in_progress + completed +} + +type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! content: String status: TodoStatus @index(sortKeyFields: ["title"], queryField: "listTodoByStatusAndTitle") -} +}" +`; -enum TodoStatus { +exports[`secondary index schema generation generates correct schema for using a.enum() as the sort key 1`] = ` +"enum TodoStatus { open in_progress completed -}" -`; +} -exports[`secondary index schema generation generates correct schema for using a.enum() as the sort key 1`] = ` -"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! @index(sortKeyFields: ["status"], queryField: "listTodoByTitleAndStatus") content: String status: TodoStatus -} +}" +`; -enum TodoStatus { +exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the partition key 1`] = ` +"enum TodoStatus { open in_progress completed -}" -`; +} -exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the partition key 1`] = ` -"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! content: String status: TodoStatus @index(sortKeyFields: ["title"], queryField: "listTodoByStatusAndTitle") -} +}" +`; -enum TodoStatus { +exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the sort key 1`] = ` +"enum TodoStatus { open in_progress completed -}" -`; +} -exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the sort key 1`] = ` -"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! @index(sortKeyFields: ["status"], queryField: "listTodoByTitleAndStatus") content: String status: TodoStatus -} - -enum TodoStatus { - open - in_progress - completed }" `; diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index 88e386a1f..966dbdf59 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -155,9 +155,7 @@ function isRefField( return isRefFieldDef((field as any)?.data); } -function canGenerateFieldType( - fieldType: ModelFieldType -): boolean { +function canGenerateFieldType(fieldType: ModelFieldType): boolean { return fieldType === 'Int'; } @@ -175,7 +173,6 @@ function scalarFieldToGql( } = fieldDef; let field: string = fieldType; - if (identifier !== undefined) { field += '!'; if (identifier.length > 1) { @@ -347,6 +344,8 @@ function customOperationToGql( ): { gqlField: string; implicitTypes: [string, any][]; + inputTypes: string[]; + returnTypes: string[]; customTypeAuthRules: CustomTypeAuthRules; lambdaFunctionDefinition: LambdaFunctionDefinition; customSqlDataSourceStrategy: CustomSqlDataSourceStrategy | undefined; @@ -361,6 +360,9 @@ function customOperationToGql( let callSignature: string = typeName; const implicitTypes: [string, any][] = []; + const inputTypes: string[] = []; + + let returnTypes: string[] = []; // When Custom Operations are defined with a Custom Type return type, // the Custom Type inherits the operation's auth rules @@ -407,7 +409,10 @@ function customOperationToGql( authRules: authorization, }; - implicitTypes.push([returnTypeName, returnType]); + implicitTypes.push([ + returnTypeName, + { ...returnType, generateInputType: false }, + ]); } return returnTypeName; } else if (isEnumType(returnType)) { @@ -444,6 +449,16 @@ function customOperationToGql( refererTypeName: typeName, }); } + // After resolving returnTypeName + if (isCustomType(returnType)) { + returnTypes = generateInputTypes( + [[returnTypeName, { ...returnType, isgenerateInputTypeInput: false }]], + false, + getRefType, + authorization, + ); + } + const dedupedInputTypes = new Set(inputTypes); if (Object.keys(fieldArgs).length > 0) { const { gqlFields, implicitTypes: implied } = processFields( @@ -451,9 +466,22 @@ function customOperationToGql( fieldArgs, {}, {}, + getRefType, + undefined, + undefined, + {}, + databaseType === 'sql' ? 'postgresql' : 'dynamodb', + true, ); callSignature += `(${gqlFields.join(', ')})`; implicitTypes.push(...implied); + + const newTypes = generateInputTypes(implied, true, getRefType); + for (const t of newTypes) { + if (!dedupedInputTypes.has(t)) { + dedupedInputTypes.add(t); + } + } } const handler = handlers && handlers[0]; @@ -550,6 +578,8 @@ function customOperationToGql( return { gqlField, implicitTypes: implicitTypes, + inputTypes, + returnTypes, customTypeAuthRules, lambdaFunctionDefinition, customSqlDataSourceStrategy, @@ -929,23 +959,33 @@ function processFieldLevelAuthRules( return fieldLevelAuthRules; } -function validateDBGeneration(fields: Record, databaseEngine: DatasourceEngine) { +function validateDBGeneration( + fields: Record, + databaseEngine: DatasourceEngine, +) { for (const [fieldName, fieldDef] of Object.entries(fields)) { const _default = fieldDef.data?.default; const fieldType = fieldDef.data?.fieldType; const isGenerated = _default === __generated; if (isGenerated && databaseEngine !== 'postgresql') { - throw new Error(`Invalid field definition for ${fieldName}. DB-generated fields are only supported with PostgreSQL data sources.`); + throw new Error( + `Invalid field definition for ${fieldName}. DB-generated fields are only supported with PostgreSQL data sources.`, + ); } if (isGenerated && !canGenerateFieldType(fieldType)) { - throw new Error(`Incompatible field type. Field type ${fieldType} in field ${fieldName} cannot be configured as a DB-generated field.`); + throw new Error( + `Incompatible field type. Field type ${fieldType} in field ${fieldName} cannot be configured as a DB-generated field.`, + ); } } } -function validateNullableIdentifiers(fields: Record, identifier?: readonly string[]){ +function validateNullableIdentifiers( + fields: Record, + identifier?: readonly string[], +) { for (const [fieldName, fieldDef] of Object.entries(fields)) { const fieldType = fieldDef.data?.fieldType; const required = fieldDef.data?.required; @@ -954,7 +994,9 @@ function validateNullableIdentifiers(fields: Record, identifier?: r if (identifier !== undefined && identifier.includes(fieldName)) { if (!required && fieldType !== 'ID' && !isGenerated) { - throw new Error(`Invalid identifier definition. Field ${fieldName} cannot be used in the identifier. Identifiers must reference required or DB-generated fields)`); + throw new Error( + `Invalid identifier definition. Field ${fieldName} cannot be used in the identifier. Identifiers must reference required or DB-generated fields)`, + ); } } } @@ -965,10 +1007,13 @@ function processFields( fields: Record, impliedFields: Record, fieldLevelAuthRules: Record, + getRefType: ReturnType, + identifier?: readonly string[], partitionKey?: string, secondaryIndexes: TransformedSecondaryIndexes = {}, databaseEngine: DatasourceEngine = 'dynamodb', + generateInputType: boolean = false, ) { const gqlFields: string[] = []; // stores nested, field-level type definitions (custom types and enums) @@ -977,7 +1022,7 @@ function processFields( validateImpliedFields(fields, impliedFields); validateDBGeneration(fields, databaseEngine); - validateNullableIdentifiers(fields, identifier) + validateNullableIdentifiers(fields, identifier); for (const [fieldName, fieldDef] of Object.entries(fields)) { const fieldAuth = fieldLevelAuthRules[fieldName] @@ -998,40 +1043,74 @@ function processFields( )}${fieldAuth}`, ); } else if (isRefField(fieldDef)) { - gqlFields.push( - `${fieldName}: ${refFieldToGql(fieldDef.data, secondaryIndexes[fieldName])}${fieldAuth}`, - ); + if (generateInputType) { + const inputTypeName = `${capitalize(typeName)}${capitalize(fieldName)}Input`; + gqlFields.push(`${fieldName}: ${inputTypeName}${fieldAuth}`); + + const refTypeInfo = getRefType(fieldDef.data.link, typeName); + + if (refTypeInfo.type === 'CustomType') { + const { implicitTypes: nestedImplicitTypes } = processFields( + inputTypeName, + refTypeInfo.def.data.fields, + {}, + {}, + getRefType, + undefined, + undefined, + {}, + 'dynamodb', + true, + ); + + implicitTypes.push([ + inputTypeName, + { + data: { + type: 'customType', + fields: refTypeInfo.def.data.fields, + }, + }, + ]); + + implicitTypes.push(...nestedImplicitTypes); + } else { + throw new Error( + `Field '${fieldName}' in type '${typeName}' references '${fieldDef.data.link}' which is not a CustomType. Check schema definitions.`, + ); + } + } else { + gqlFields.push( + `${fieldName}: ${refFieldToGql(fieldDef.data, secondaryIndexes[fieldName])}${fieldAuth}`, + ); + } } else if (isEnumType(fieldDef)) { // The inline enum type name should be `` to avoid // enum type name conflicts const enumName = `${capitalize(typeName)}${capitalize(fieldName)}`; - implicitTypes.push([enumName, fieldDef]); - gqlFields.push( `${fieldName}: ${enumFieldToGql(enumName, secondaryIndexes[fieldName])}`, ); } else if (isCustomType(fieldDef)) { // The inline CustomType name should be `` to avoid // CustomType name conflicts - const customTypeName = `${capitalize(typeName)}${capitalize( - fieldName, - )}`; - + const customTypeName = `${capitalize(typeName)}${capitalize(fieldName)}${generateInputType ? 'Input' : ''}`; implicitTypes.push([customTypeName, fieldDef]); - - gqlFields.push(`${fieldName}: ${customTypeName}`); + gqlFields.push(`${fieldName}: ${customTypeName}${fieldAuth}`); } else { gqlFields.push( `${fieldName}: ${scalarFieldToGql( - (fieldDef as any).data, + fieldDef.data, undefined, secondaryIndexes[fieldName], )}${fieldAuth}`, ); } } else { - throw new Error(`Unexpected field definition: ${fieldDef}`); + throw new Error( + `Unexpected field definition for ${typeName}.${fieldName}: ${JSON.stringify(fieldDef)}`, + ); } } @@ -1307,6 +1386,50 @@ const mergeCustomTypeAuthRules = ( } }; +function generateInputTypes( + implicitTypes: [string, any][], + generateInputType: boolean, + getRefType: ReturnType, + authRules?: Authorization[], + isInlineType = false, +): string[] { + const generatedTypes = new Set(); + + implicitTypes.forEach(([typeName, typeDef]) => { + if (isCustomType(typeDef)) { + const { gqlFields } = processFields( + typeName, + typeDef.data.fields, + {}, + {}, + getRefType, + undefined, + undefined, + {}, + 'dynamodb', + generateInputType, + ); + const authString = + !isInlineType && authRules + ? mapToNativeAppSyncAuthDirectives(authRules, false).authString + : ''; + const typeKeyword = generateInputType ? 'input' : 'type'; + const customType = `${typeKeyword} ${typeName}${authString ? ` ${authString}` : ''}\n{\n ${gqlFields.join('\n ')}\n}`; + generatedTypes.add(customType); + } else if (typeDef.type === 'enum') { + const enumDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; + generatedTypes.add(enumDefinition); + } else if (typeDef.type === 'scalar') { + const scalarDefinition = `scalar ${typeName}`; + generatedTypes.add(scalarDefinition); + } else { + console.warn(`Unexpected type definition for ${typeName}:`, typeDef); + } + }); + + return Array.from(generatedTypes); +} + const schemaPreprocessor = ( schema: InternalSchema, ): { @@ -1316,7 +1439,9 @@ const schemaPreprocessor = ( lambdaFunctions: LambdaFunctionDefinition; customSqlDataSourceStrategies?: CustomSqlDataSourceStrategy[]; } => { + const enumTypes = new Set(); const gqlModels: string[] = []; + const inputTypes: string[] = []; const customQueries = []; const customMutations = []; @@ -1334,11 +1459,8 @@ const schemaPreprocessor = ( const lambdaFunctions: LambdaFunctionDefinition = {}; const customSqlDataSourceStrategies: CustomSqlDataSourceStrategy[] = []; - const databaseEngine = schema.data.configuration.database.engine - const databaseType = - databaseEngine === 'dynamodb' - ? 'dynamodb' - : 'sql'; + const databaseEngine = schema.data.configuration.database.engine; + const databaseType = databaseEngine === 'dynamodb' ? 'dynamodb' : 'sql'; const staticSchema = databaseType === 'sql'; @@ -1382,10 +1504,8 @@ const schemaPreprocessor = ( `Values of the enum type ${typeName} should not contain any whitespace.`, ); } - const enumType = `enum ${typeName} {\n ${typeDef.values.join( - '\n ', - )}\n}`; - gqlModels.push(enumType); + const enumType = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; + enumTypes.add(enumType); } else if (isCustomType(typeDef)) { const fields = typeDef.data.fields; @@ -1419,14 +1539,20 @@ const schemaPreprocessor = ( fields, authFields, fieldLevelAuthRules, + getRefType, undefined, undefined, - undefined, - databaseEngine + databaseEngine, ); - - topLevelTypes.push(...implicitTypes); - + const existingTypeNames = new Set( + topLevelTypes.map(([n]) => n), + ); + for (const [name, type] of implicitTypes) { + if (!existingTypeNames.has(name)) { + topLevelTypes.push([name, type]); + existingTypeNames.add(name); + } + } const joined = gqlFields.join('\n '); const model = `type ${typeName} ${customAuth}\n{\n ${joined}\n}`; @@ -1439,6 +1565,8 @@ const schemaPreprocessor = ( const { gqlField, implicitTypes, + inputTypes: operationInputTypes, + returnTypes: operationReturnTypes, customTypeAuthRules, jsFunctionForField, lambdaFunctionDefinition, @@ -1450,8 +1578,39 @@ const schemaPreprocessor = ( databaseType, getRefType, ); - - topLevelTypes.push(...implicitTypes); + inputTypes.push(...operationInputTypes); + gqlModels.push(...operationReturnTypes); + + /** + * Processes implicit types to generate GraphQL definitions. + * + * - Enums are converted to 'enum' definitions and added to the schema. + * - Custom types are conditionally treated as input types if they are + * not part of the operation's return types. + * - Input types are generated and added to the inputTypes array when required. + * + * This ensures that all necessary type definitions, including enums and input types, + * are correctly generated and available in the schema output. + */ + + implicitTypes.forEach(([typeName, typeDef]) => { + if (isEnumType(typeDef)) { + const enumTypeDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; + enumTypes.add(enumTypeDefinition); + } else { + const shouldGenerateInputType = !operationReturnTypes.some( + (returnType) => returnType.includes(typeName), + ); + const generatedTypeDefinition = generateInputTypes( + [[typeName, typeDef]], + shouldGenerateInputType, + getRefType, + )[0]; + if (shouldGenerateInputType) { + inputTypes.push(generatedTypeDefinition); + } + } + }); mergeCustomTypeAuthRules( customTypeInheritedAuthRules, @@ -1531,12 +1690,19 @@ const schemaPreprocessor = ( fields, authFields, fieldLevelAuthRules, + getRefType, identifier, partitionKey, undefined, databaseEngine, ); - + const existingTypeNames = new Set(topLevelTypes.map(([n]) => n)); + for (const [name, type] of implicitTypes) { + if (!existingTypeNames.has(name)) { + topLevelTypes.push([name, type]); + existingTypeNames.add(name); + } + } topLevelTypes.push(...implicitTypes); const joined = gqlFields.join('\n '); @@ -1596,6 +1762,7 @@ const schemaPreprocessor = ( fields, authFields, fieldLevelAuthRules, + getRefType, identifier, partitionKey, transformedSecondaryIndexes, @@ -1622,12 +1789,21 @@ const schemaPreprocessor = ( subscriptions: customSubscriptions, }; - gqlModels.push(...generateCustomOperationTypes(customOperations)); + const customOperationTypes = generateCustomOperationTypes(customOperations); + + const schemaComponents = [ + ...Array.from(enumTypes), + ...gqlModels, + ...customOperationTypes, + ]; + if (shouldAddConversationTypes) { - gqlModels.push(CONVERSATION_SCHEMA_GRAPHQL_TYPES); + schemaComponents.push(CONVERSATION_SCHEMA_GRAPHQL_TYPES); } - const processedSchema = gqlModels.join('\n\n'); + schemaComponents.push(...inputTypes); + + const processedSchema = schemaComponents.join('\n\n'); return { schema: processedSchema, @@ -1926,6 +2102,8 @@ function transformCustomOperations( const { gqlField, implicitTypes, + inputTypes, + returnTypes, customTypeAuthRules, lambdaFunctionDefinition, customSqlDataSourceStrategy, @@ -1941,6 +2119,8 @@ function transformCustomOperations( return { gqlField, implicitTypes, + inputTypes, + returnTypes, customTypeAuthRules, jsFunctionForField, lambdaFunctionDefinition, @@ -1975,7 +2155,14 @@ function extractNestedCustomTypeNames( topLevelTypes: [string, any][], getRefType: ReturnType, ): string[] { - if (!customTypeAuthRules) { + if (!customTypeAuthRules || !topLevelTypes || topLevelTypes.length === 0) { + return []; + } + const foundType = topLevelTypes.find( + ([topLevelTypeName]) => customTypeAuthRules.typeName === topLevelTypeName, + ); + + if (!foundType) { return []; } diff --git a/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap b/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap index f35e2fdc1..14cdf30bc 100644 --- a/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap +++ b/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ClientSchema overview sample schema 1`] = ` -"type Customer @model @auth(rules: [{allow: public, provider: apiKey}]) +"enum CustomerEngagementStage { + PROSPECT + INTERESTED + PURCHASED +} + +type Customer @model @auth(rules: [{allow: public, provider: apiKey}]) { customerId: ID! @primaryKey name: String @@ -22,11 +28,5 @@ type CustomerLocation { lat: Float! long: Float! -} - -enum CustomerEngagementStage { - PROSPECT - INTERESTED - PURCHASED }" `; diff --git a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap index d9625d55f..270c95500 100644 --- a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap +++ b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap @@ -40,6 +40,86 @@ exports[`custom operations async sync 1`] = ` ] `; +exports[`custom operations client operations with custom types and refs client can call custom mutation with custom type argument 1`] = ` +[ + [ + { + "authMode": undefined, + "authToken": undefined, + "query": " + mutation($customArg: MutateWithCustomTypeArgCustomArgInput) { + mutateWithCustomTypeArg(customArg: $customArg) + } + ", + "variables": { + "customArg": {}, + }, + }, + {}, + ], +] +`; + +exports[`custom operations client operations with custom types and refs client can call custom mutation with ref argument 1`] = ` +[ + [ + { + "authMode": undefined, + "authToken": undefined, + "query": " + mutation($refArg: MutationWithRefArgRefArgInput) { + mutationWithRefArg(refArg: $refArg) + } + ", + "variables": { + "refArg": {}, + }, + }, + {}, + ], +] +`; + +exports[`custom operations client operations with custom types and refs client can call custom query with custom type argument 1`] = ` +[ + [ + { + "authMode": undefined, + "authToken": undefined, + "query": " + query($customArg: QueryWithCustomTypeArgCustomArgInput) { + queryWithCustomTypeArg(customArg: $customArg) + } + ", + "variables": { + "customArg": {}, + }, + }, + {}, + ], +] +`; + +exports[`custom operations client operations with custom types and refs client can call custom query with ref argument 1`] = ` +[ + [ + { + "authMode": undefined, + "authToken": undefined, + "query": " + query($refArg: QueryWithRefArgRefArgInput) { + queryWithRefArg(refArg: $refArg) + } + ", + "variables": { + "refArg": {}, + }, + }, + {}, + ], +] +`; + exports[`custom operations custom type array result 1`] = ` [ [ diff --git a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts index 2f9bced61..4a00b7f05 100644 --- a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts +++ b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts @@ -123,13 +123,22 @@ describe('custom operations', () => { a.handler.function(dummyHandler).async(), ]) .authorization((allow) => [allow.publicApiKey()]), + CustomArgType: a.customType({ + message: a.string(), + count: a.integer(), + }), + NestedObjectType: a.customType({ + innerField1: a.boolean(), + innerField2: a.string(), + }), + + NestedFieldType: a.customType({ + nestedObject1: a.ref('NestedObjectType'), + }), queryWithCustomTypeArg: a .query() .arguments({ - customArg: a.customType({ - message: a.string(), - count: a.integer(), - }), + customArg: a.ref('CustomArgType'), }) .returns(a.string()) .handler(a.handler.function(dummyHandler)) @@ -137,10 +146,7 @@ describe('custom operations', () => { mutateWithCustomTypeArg: a .mutation() .arguments({ - customArg: a.customType({ - message: a.string(), - count: a.integer(), - }), + customArg: a.ref('CustomArgType'), }) .returns(a.string()) .handler(a.handler.function(dummyHandler)) @@ -148,12 +154,7 @@ describe('custom operations', () => { mutationWithNestedCustomType: a .mutation() .arguments({ - nestedField: a.customType({ - nestedObject1: a.customType({ - innerField1: a.boolean(), - innerField2: a.string(), - }), - }), + nestedField: a.ref('NestedFieldType'), }) .returns(a.string()) .handler(a.handler.function(dummyHandler)) @@ -174,14 +175,15 @@ describe('custom operations', () => { .returns(a.string()) .handler(a.handler.function(dummyHandler)) .authorization((allow) => [allow.publicApiKey()]), + ComplexCustomArgType: a.customType({ + field1: a.string(), + field2: a.integer(), + }), complexQueryOperation: a .query() .arguments({ scalarArg: a.string(), - customArg: a.customType({ - field1: a.string(), - field2: a.integer(), - }), + customArg: a.ref('ComplexCustomArgType'), refArg: a.ref('EchoResult'), }) .returns(a.string()) @@ -191,10 +193,7 @@ describe('custom operations', () => { .mutation() .arguments({ scalarArg: a.string(), - customArg: a.customType({ - field1: a.string(), - field2: a.integer(), - }), + customArg: a.ref('ComplexCustomArgType'), refArg: a.ref('EchoResult'), }) .returns(a.string()) @@ -261,7 +260,7 @@ describe('custom operations', () => { Equal >; - type ExpectedComplexArgs = { + type ExpectedComplexQueryArgs = { scalarArg?: string | null; customArg?: { field1?: string | null; @@ -272,7 +271,9 @@ describe('custom operations', () => { } | null; }; type ActualComplexArgs = Schema['complexQueryOperation']['args']; - type TestComplexArgs = Expect>; + type TestComplexArgs = Expect< + Equal + >; type ExpectedComplexMutationArgs = { scalarArg?: string | null; @@ -290,7 +291,35 @@ describe('custom operations', () => { >; // #endregion - test.skip('primitive type result', async () => { + test('schema.transform() includes custom types, ref types, and operations', () => { + const transformedSchema = schema.transform(); + const expectedTypes = ['CustomArgType', 'EchoResult', 'Query', 'Mutation']; + const expectedOperations = [ + 'queryWithCustomTypeArg(customArg: QueryWithCustomTypeArgCustomArgInput): String', + 'queryWithRefArg(refArg: QueryWithRefArgRefArgInput): String', + 'mutateWithCustomTypeArg(customArg: MutateWithCustomTypeArgCustomArgInput): String', + 'mutationWithRefArg(refArg: MutationWithRefArgRefArgInput): String', + ]; + const expectedInputTypes = [ + 'input QueryWithCustomTypeArgCustomArgInput', + 'input QueryWithRefArgRefArgInput', + 'input MutateWithCustomTypeArgCustomArgInput', + 'input MutationWithRefArgRefArgInput', + ]; + + expectedTypes.forEach((type) => { + expect(transformedSchema.schema).toContain(`type ${type}`); + }); + + expectedOperations.forEach((operation) => { + expect(transformedSchema.schema).toContain(operation); + }); + + expectedInputTypes.forEach((inputType) => { + expect(transformedSchema.schema).toContain(inputType); + }); + }); + test('primitive type result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -302,7 +331,6 @@ describe('custom operations', () => { const config = await buildAmplifyConfig(schema); Amplify.configure(config); const client = generateClient(); - // #region covers ffefd700b1e323c9 const { data } = await client.queries.echo({ value: 'something' }); // #endregion @@ -311,7 +339,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('custom type result', async () => { + test('custom type result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -336,7 +364,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('custom type array result', async () => { + test('custom type array result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -375,7 +403,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('model result', async () => { + test('model result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -407,7 +435,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('model array result', async () => { + test('model array result', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -459,7 +487,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('solo async handler', async () => { + test('solo async handler', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -487,7 +515,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('async sync', async () => { + test('async sync', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -513,7 +541,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('sync sync', async () => { + test('sync sync', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -539,7 +567,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('sync async', async () => { + test('sync async', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -567,7 +595,7 @@ describe('custom operations', () => { expect(optionsAndHeaders(spy)).toMatchSnapshot(); }); - test.skip('async async', async () => { + test('async async', async () => { const { spy, generateClient } = mockedGenerateClient([ { data: { @@ -701,4 +729,85 @@ describe('custom operations', () => { const { data } = await client.queries.echoEnum({ status: 'BAD VALUE' }); }); }); + describe('client operations with custom types and refs', () => { + test('client can call custom query with custom type argument', async () => { + const { spy, generateClient } = mockedGenerateClient([ + { + data: { + queryWithCustomTypeArg: 'Custom type query result', + }, + }, + ]); + + const config = await buildAmplifyConfig(schema); + Amplify.configure(config); + const client = generateClient(); + + const { data } = await client.queries.queryWithCustomTypeArg({ + customArg: {}, + }); + expect(data).toEqual('Custom type query result'); + expect(optionsAndHeaders(spy)).toMatchSnapshot(); + }); + + test('client can call custom query with ref argument', async () => { + const { spy, generateClient } = mockedGenerateClient([ + { + data: { + queryWithRefArg: 'Ref type query result', + }, + }, + ]); + + const config = await buildAmplifyConfig(schema); + Amplify.configure(config); + const client = generateClient(); + + const { data } = await client.queries.queryWithRefArg({ + refArg: {}, + }); + expect(data).toEqual('Ref type query result'); + expect(optionsAndHeaders(spy)).toMatchSnapshot(); + }); + + test('client can call custom mutation with custom type argument', async () => { + const { spy, generateClient } = mockedGenerateClient([ + { + data: { + mutateWithCustomTypeArg: 'Custom type mutation result', + }, + }, + ]); + + const config = await buildAmplifyConfig(schema); + Amplify.configure(config); + const client = generateClient(); + + const { data } = await client.mutations.mutateWithCustomTypeArg({ + customArg: {}, + }); + expect(data).toEqual('Custom type mutation result'); + expect(optionsAndHeaders(spy)).toMatchSnapshot(); + }); + + test('client can call custom mutation with ref argument', async () => { + const { spy, generateClient } = mockedGenerateClient([ + { + data: { + mutationWithRefArg: 'Ref type mutation result', + }, + }, + ]); + + const config = await buildAmplifyConfig(schema); + Amplify.configure(config); + const client = generateClient(); + + const { data } = await client.mutations.mutationWithRefArg({ + refArg: {}, + }); + expect(data).toEqual('Ref type mutation result'); + expect(optionsAndHeaders(spy)).toMatchSnapshot(); + }); + }); }); From c54ed9bbb5ff47524620a34c5307fd24d487a592 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Tue, 14 Jan 2025 20:20:04 -0800 Subject: [PATCH 05/12] Fix: Generate nested CustomTypes in schema output --- packages/data-schema/src/SchemaProcessor.ts | 34 ++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index 966dbdf59..6718b5449 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -1397,7 +1397,7 @@ function generateInputTypes( implicitTypes.forEach(([typeName, typeDef]) => { if (isCustomType(typeDef)) { - const { gqlFields } = processFields( + const { gqlFields, implicitTypes: nestedTypes } = processFields( typeName, typeDef.data.fields, {}, @@ -1416,6 +1416,20 @@ function generateInputTypes( const typeKeyword = generateInputType ? 'input' : 'type'; const customType = `${typeKeyword} ${typeName}${authString ? ` ${authString}` : ''}\n{\n ${gqlFields.join('\n ')}\n}`; generatedTypes.add(customType); + + // Process nested types + if (nestedTypes.length > 0) { + const nestedGeneratedTypes = generateInputTypes( + nestedTypes, + generateInputType, + getRefType, + authRules, + true, + ); + nestedGeneratedTypes.forEach((type) => { + generatedTypes.add(type); + }); + } } else if (typeDef.type === 'enum') { const enumDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; generatedTypes.add(enumDefinition); @@ -1441,7 +1455,7 @@ const schemaPreprocessor = ( } => { const enumTypes = new Set(); const gqlModels: string[] = []; - const inputTypes: string[] = []; + const inputTypes = new Map(); const customQueries = []; const customMutations = []; @@ -1578,7 +1592,10 @@ const schemaPreprocessor = ( databaseType, getRefType, ); - inputTypes.push(...operationInputTypes); + operationInputTypes.forEach((type) => { + const typeName = type.split(' ')[1]; + inputTypes.set(typeName, type); + }); gqlModels.push(...operationReturnTypes); /** @@ -1601,13 +1618,16 @@ const schemaPreprocessor = ( const shouldGenerateInputType = !operationReturnTypes.some( (returnType) => returnType.includes(typeName), ); - const generatedTypeDefinition = generateInputTypes( + const generatedTypeDefinitions = generateInputTypes( [[typeName, typeDef]], shouldGenerateInputType, getRefType, - )[0]; + ); if (shouldGenerateInputType) { - inputTypes.push(generatedTypeDefinition); + generatedTypeDefinitions.forEach((def) => { + const defName = def.split(' ')[1]; + inputTypes.set(defName, def); + }); } } }); @@ -1801,7 +1821,7 @@ const schemaPreprocessor = ( schemaComponents.push(CONVERSATION_SCHEMA_GRAPHQL_TYPES); } - schemaComponents.push(...inputTypes); + schemaComponents.push(...inputTypes.values()); const processedSchema = schemaComponents.join('\n\n'); From cc50e2064e30242633cbdef77cfd2e79256e09c0 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Wed, 15 Jan 2025 10:17:34 -0800 Subject: [PATCH 06/12] refactor: Add tests for nested custom types and mixed refs --- .../__tests__/CustomOperations.test-d.ts | 108 +++++++++++++++--- 1 file changed, 93 insertions(+), 15 deletions(-) diff --git a/packages/data-schema/__tests__/CustomOperations.test-d.ts b/packages/data-schema/__tests__/CustomOperations.test-d.ts index 65bbd936e..fd9874064 100644 --- a/packages/data-schema/__tests__/CustomOperations.test-d.ts +++ b/packages/data-schema/__tests__/CustomOperations.test-d.ts @@ -16,9 +16,7 @@ import type { import { configure } from '../src/ModelSchema'; import { Nullable } from '../src/ModelField'; import { defineFunctionStub } from './utils'; -import type { - CustomOperation, -} from '../src/CustomOperation'; +import type { CustomOperation } from '../src/CustomOperation'; describe('custom operations return types', () => { describe('when .ref() a basic custom type', () => { @@ -87,7 +85,7 @@ describe('custom operations return types', () => { }); it('produces async function handler types', () => { - const handler = defineFunctionStub({}) + const handler = defineFunctionStub({}); const schema = a.schema({ aQuery: a .query() @@ -95,7 +93,7 @@ describe('custom operations return types', () => { .arguments({ input: a.string(), content: a.string().required(), - }) + }), }); type Schema = ClientSchema; @@ -112,10 +110,7 @@ describe('custom operations return types', () => { type ExpectedResult = { success: boolean; } | null; - type ExpectedFunctionHandler = AppSyncResolverHandler< - ActualArgs, - void - >; + type ExpectedFunctionHandler = AppSyncResolverHandler; type _T1 = Expect>; type _T2 = Expect>; type _T3 = Expect>; @@ -865,18 +860,101 @@ describe('.for() modifier', () => { describe('.arguments() modifier', () => { // Test to verify that CustomType can be used as an argument in custom operations it('accepts CustomType in arguments', () => { - const operation: CustomOperation = a.query().arguments({ + const operation: CustomOperation< + any, + 'arguments' | 'for', + 'queryCustomOperation' + > = a.query().arguments({ customArg: a.customType({ field1: a.string(), - field2: a.integer() - }) + field2: a.integer(), + }), }); }); // Test to verify that RefType can be used as an argument in custom operations it('accepts RefType in arguments', () => { - const operation:CustomOperation = a.query().arguments({ - refArg: a.ref('SomeType') + const operation: CustomOperation< + any, + 'arguments' | 'for', + 'queryCustomOperation' + > = a.query().arguments({ + refArg: a.ref('SomeType'), }); }); -}); \ No newline at end of file + + it('handles deeply nested custom types', () => { + const schema = a.schema({ + DeepNested: a.customType({ + level1: a.customType({ + level2: a.customType({ + level3: a.string(), + }), + }), + }), + deepQuery: a + .query() + .arguments({ + input: a.ref('DeepNested'), + }) + .returns(a.string()), + }); + + type Schema = ClientSchema; + + type ExpectedArgs = { + input?: { + level1?: { + level2?: { + level3?: string | null; + } | null; + } | null; + } | null; + }; + + type ActualArgs = Schema['deepQuery']['args']; + + type Test = Expect>; + }); + + it('handles mixed custom types and refs', () => { + const schema = a.schema({ + RefType: a.customType({ + field: a.string(), + }), + MixedType: a.customType({ + nested: a.customType({ + refField: a.ref('RefType'), + customField: a.customType({ + deepField: a.integer(), + }), + }), + }), + mixedQuery: a + .query() + .arguments({ + input: a.ref('MixedType'), + }) + .returns(a.string()), + }); + + type Schema = ClientSchema; + + type ExpectedArgs = { + input?: { + nested?: { + refField?: { + field?: string | null; + } | null; + customField?: { + deepField?: number | null; + } | null; + } | null; + } | null; + }; + + type ActualArgs = Schema['mixedQuery']['args']; + + type Test = Expect>; + }); +}); From fff0b5d6f57cb594f5ff25a28470e8de5e8b3e9d Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Wed, 15 Jan 2025 21:45:08 -0800 Subject: [PATCH 07/12] Add test for RefType with nested CustomType in complex structures --- .../__tests__/CustomOperations.test-d.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/data-schema/__tests__/CustomOperations.test-d.ts b/packages/data-schema/__tests__/CustomOperations.test-d.ts index fd9874064..455c167d8 100644 --- a/packages/data-schema/__tests__/CustomOperations.test-d.ts +++ b/packages/data-schema/__tests__/CustomOperations.test-d.ts @@ -957,4 +957,44 @@ describe('.arguments() modifier', () => { type Test = Expect>; }); + + it('handles RefType with multi-layered custom types in nested structures', () => { + const schema = a.schema({ + NestedCustomType: a.customType({ + nestedField: a.string(), + }), + RefType: a.customType({ + field: a.string(), + nestedCustom: a.ref('NestedCustomType'), + }), + OuterType: a.customType({ + refField: a.ref('RefType'), + otherField: a.integer(), + }), + complexQuery: a + .query() + .arguments({ + input: a.ref('OuterType'), + }) + .returns(a.string()), + }); + + type Schema = ClientSchema; + + type ExpectedArgs = { + input?: { + refField?: { + field?: string | null; + nestedCustom?: { + nestedField?: string | null; + } | null; + } | null; + otherField?: number | null; + } | null; + }; + + type ActualArgs = Schema['complexQuery']['args']; + + type Test = Expect>; + }); }); From 949e6ee178b77d4db49fff46e7a96577fc10fbe7 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Thu, 16 Jan 2025 20:58:58 -0800 Subject: [PATCH 08/12] fix: Adjust schema component ordering to minimize enum reordering --- .../__snapshots__/ClientSchema.test.ts.snap | 26 ++--- .../CustomOperations.test.ts.snap | 12 +-- .../__snapshots__/CustomType.test.ts.snap | 96 +++++++++---------- .../__snapshots__/EnumType.test.ts.snap | 48 +++++----- .../__snapshots__/ModelIndex.test.ts.snap | 50 +++++----- packages/data-schema/src/SchemaProcessor.ts | 6 +- .../__snapshots__/client-schema.ts.snap | 14 +-- 7 files changed, 125 insertions(+), 127 deletions(-) diff --git a/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap index d4d7d0b0f..2273c0366 100644 --- a/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap @@ -243,14 +243,7 @@ type Query { `; exports[`custom operations Add entities to SQL schema add custom type, enum, and custom query to generated SQL schema 1`] = ` -"enum PostStatus { - draft - pending - approved - published -} - -type post @model(timestamps: null) @auth(rules: [{allow: private}]) +"type post @model(timestamps: null) @auth(rules: [{allow: private}]) { title: String! description: String @@ -263,6 +256,13 @@ type PostMeta @aws_cognito_user_pools approvedOn: AWSDate } +enum PostStatus { + draft + pending + approved + published +} + type Query { getPostMeta(id: String): PostMeta @sql(statement: "SELECT viewCount, approvedOn FROM some_table") @auth(rules: [{allow: private}]) }" @@ -596,11 +596,7 @@ exports[`schema auth rules global public auth - multiple models 1`] = ` "functionSlots": [], "jsFunctions": [], "lambdaFunctions": {}, - "schema": "enum DTired { - ? -} - -type A @model @auth(rules: [{allow: public, provider: apiKey}]) + "schema": "type A @model @auth(rules: [{allow: public, provider: apiKey}]) { field: String } @@ -626,6 +622,10 @@ type D @model @auth(rules: [{allow: public, provider: apiKey}]) tired: DTired cId: ID c: C @belongsTo(references: ["cId"]) +} + +enum DTired { + ? }", } `; diff --git a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap index a81507f10..5832064c1 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap @@ -1,16 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CustomOperation transform dynamo schema Custom Mutation w required arg and enum 1`] = ` -"enum LikePostReactionType { - :shipit: - :risitas: -} - -type Post @model @auth(rules: [{allow: private}]) +"type Post @model @auth(rules: [{allow: private}]) { title: String } +enum LikePostReactionType { + :shipit: + :risitas: +} + type Mutation { likePost(postId: String!, reactionType: LikePostReactionType): Post }" diff --git a/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap index f7b601e28..a396d9323 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap @@ -40,12 +40,7 @@ type Location `; exports[`CustomType transform Explicit CustomType nests explicit CustomType 1`] = ` -"enum PostStatus { - unpublished - published -} - -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: Meta } @@ -59,16 +54,16 @@ type Meta type AltMeta { field1: String -}" -`; +} -exports[`CustomType transform Explicit CustomType nests explicit enum 1`] = ` -"enum PostStatus { +enum PostStatus { unpublished published -} +}" +`; -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`CustomType transform Explicit CustomType nests explicit enum 1`] = ` +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: Meta } @@ -77,16 +72,16 @@ type Meta { status: PostStatus publishedDate: AWSDate -}" -`; +} -exports[`CustomType transform Explicit CustomType nests implicit CustomType 1`] = ` -"enum MetaStatus { +enum PostStatus { unpublished published -} +}" +`; -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`CustomType transform Explicit CustomType nests implicit CustomType 1`] = ` +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: Meta } @@ -100,16 +95,16 @@ type Meta type MetaNestedMeta { field1: String -}" -`; +} -exports[`CustomType transform Explicit CustomType nests implicit enum type 1`] = ` -"enum MetaStatus { +enum MetaStatus { unpublished published -} +}" +`; -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`CustomType transform Explicit CustomType nests implicit enum type 1`] = ` +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: Meta } @@ -118,6 +113,11 @@ type Meta { status: MetaStatus publishedDate: AWSDate +} + +enum MetaStatus { + unpublished + published }" `; @@ -135,12 +135,7 @@ type PostLocation `; exports[`CustomType transform Implicit CustomType nests explicit CustomType 1`] = ` -"enum PostMetaStatus { - unpublished - published -} - -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -154,16 +149,16 @@ type PostMeta { status: PostMetaStatus nestedMeta: AltMeta -}" -`; +} -exports[`CustomType transform Implicit CustomType nests explicit enum 1`] = ` -"enum PostStatus { +enum PostMetaStatus { unpublished published -} +}" +`; -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`CustomType transform Implicit CustomType nests explicit enum 1`] = ` +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -172,16 +167,16 @@ type PostMeta { status: PostStatus publishedDate: AWSDate -}" -`; +} -exports[`CustomType transform Implicit CustomType nests implicit CustomType 1`] = ` -"enum PostMetaStatus { +enum PostStatus { unpublished published -} +}" +`; -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`CustomType transform Implicit CustomType nests implicit CustomType 1`] = ` +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -195,16 +190,16 @@ type PostMeta type PostMetaNestedMeta { field1: String -}" -`; +} -exports[`CustomType transform Implicit CustomType nests implicit enum type 1`] = ` -"enum PostMetaStatus { +enum PostMetaStatus { unpublished published -} +}" +`; -type Post @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`CustomType transform Implicit CustomType nests implicit enum type 1`] = ` +"type Post @model @auth(rules: [{allow: public, provider: apiKey}]) { meta: PostMeta } @@ -213,5 +208,10 @@ type PostMeta { status: PostMetaStatus publishedDate: AWSDate +} + +enum PostMetaStatus { + unpublished + published }" `; diff --git a/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap index 2492d578d..524a00326 100644 --- a/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/EnumType.test.ts.snap @@ -1,53 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EnumType transform Explicit Enum - auth 1`] = ` -"enum AccessLevel { +"type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: AccessLevel @auth(rules: [{allow: owner, ownerField: "owner"}]) +} + +enum AccessLevel { public protected private -} - -type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: AccessLevel @auth(rules: [{allow: owner, ownerField: "owner"}]) }" `; exports[`EnumType transform Explicit Enum - required 1`] = ` -"enum AccessLevel { +"type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: AccessLevel! +} + +enum AccessLevel { public protected private -} - -type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: AccessLevel! }" `; exports[`EnumType transform Explicit Enum 1`] = ` -"enum AccessLevel { +"type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: AccessLevel +} + +enum AccessLevel { public protected private -} - -type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: AccessLevel }" `; exports[`EnumType transform Implicit Enum 1`] = ` -"enum FileAccessLevel { +"type File @model @auth(rules: [{allow: public, provider: apiKey}]) +{ + accessLevel: FileAccessLevel +} + +enum FileAccessLevel { public protected private -} - -type File @model @auth(rules: [{allow: public, provider: apiKey}]) -{ - accessLevel: FileAccessLevel }" `; diff --git a/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap index 112cf923f..913272736 100644 --- a/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/ModelIndex.test.ts.snap @@ -1,61 +1,61 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`secondary index schema generation generates correct schema for using a.enum() as the partition key 1`] = ` -"enum TodoStatus { - open - in_progress - completed -} - -type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! content: String status: TodoStatus @index(sortKeyFields: ["title"], queryField: "listTodoByStatusAndTitle") -}" -`; +} -exports[`secondary index schema generation generates correct schema for using a.enum() as the sort key 1`] = ` -"enum TodoStatus { +enum TodoStatus { open in_progress completed -} +}" +`; -type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`secondary index schema generation generates correct schema for using a.enum() as the sort key 1`] = ` +"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! @index(sortKeyFields: ["status"], queryField: "listTodoByTitleAndStatus") content: String status: TodoStatus -}" -`; +} -exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the partition key 1`] = ` -"enum TodoStatus { +enum TodoStatus { open in_progress completed -} +}" +`; -type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the partition key 1`] = ` +"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! content: String status: TodoStatus @index(sortKeyFields: ["title"], queryField: "listTodoByStatusAndTitle") -}" -`; +} -exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the sort key 1`] = ` -"enum TodoStatus { +enum TodoStatus { open in_progress completed -} +}" +`; -type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) +exports[`secondary index schema generation generates correct schema for using a.ref() (refer to an enum) as the sort key 1`] = ` +"type Todo @model @auth(rules: [{allow: public, provider: apiKey}]) { title: String! @index(sortKeyFields: ["status"], queryField: "listTodoByTitleAndStatus") content: String status: TodoStatus +} + +enum TodoStatus { + open + in_progress + completed }" `; diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index 6718b5449..bb710d6d3 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -1812,17 +1812,15 @@ const schemaPreprocessor = ( const customOperationTypes = generateCustomOperationTypes(customOperations); const schemaComponents = [ - ...Array.from(enumTypes), ...gqlModels, + ...Array.from(enumTypes), ...customOperationTypes, + ...Array.from(inputTypes.values()), ]; if (shouldAddConversationTypes) { schemaComponents.push(CONVERSATION_SCHEMA_GRAPHQL_TYPES); } - - schemaComponents.push(...inputTypes.values()); - const processedSchema = schemaComponents.join('\n\n'); return { diff --git a/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap b/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap index 14cdf30bc..f35e2fdc1 100644 --- a/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap +++ b/packages/integration-tests/__tests__/defined-behavior/1-patterns/__snapshots__/client-schema.ts.snap @@ -1,13 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ClientSchema overview sample schema 1`] = ` -"enum CustomerEngagementStage { - PROSPECT - INTERESTED - PURCHASED -} - -type Customer @model @auth(rules: [{allow: public, provider: apiKey}]) +"type Customer @model @auth(rules: [{allow: public, provider: apiKey}]) { customerId: ID! @primaryKey name: String @@ -28,5 +22,11 @@ type CustomerLocation { lat: Float! long: Float! +} + +enum CustomerEngagementStage { + PROSPECT + INTERESTED + PURCHASED }" `; From cdb02d17605dd669f86607fc6412a185f0075a08 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Thu, 16 Jan 2025 23:46:09 -0800 Subject: [PATCH 09/12] Revert unnecessary changes for custom types and refs --- .../__tests__/CustomOperations.test.ts | 60 +++++++------------ .../CustomOperations.test.ts.snap | 12 ---- packages/data-schema/src/SchemaProcessor.ts | 2 +- 3 files changed, 21 insertions(+), 53 deletions(-) diff --git a/packages/data-schema/__tests__/CustomOperations.test.ts b/packages/data-schema/__tests__/CustomOperations.test.ts index a841ed336..ca9820776 100644 --- a/packages/data-schema/__tests__/CustomOperations.test.ts +++ b/packages/data-schema/__tests__/CustomOperations.test.ts @@ -1341,14 +1341,15 @@ describe('custom operations + custom type auth inheritance', () => { test('implicit custom type inherits auth rules from referencing op', () => { const s = a.schema({ - MyQueryReturnType: a.customType({ - fieldA: a.string(), - fieldB: a.integer(), - }), myQuery: a .query() .handler(a.handler.function('myFn')) - .returns(a.ref('MyQueryReturnType')) + .returns( + a.customType({ + fieldA: a.string(), + fieldB: a.integer(), + }), + ) .authorization((allow) => allow.publicApiKey()), }); @@ -1362,22 +1363,23 @@ describe('custom operations + custom type auth inheritance', () => { test('nested custom types inherit auth rules from top-level referencing op', () => { const s = a.schema({ - MyQueryReturnType: a.customType({ - fieldA: a.string(), - fieldB: a.integer(), - nestedCustomType: a.customType({ - nestedA: a.string(), - nestedB: a.string(), - grandChild: a.customType({ - grandA: a.string(), - grandB: a.string(), - }), - }), - }), myQuery: a .query() .handler(a.handler.function('myFn')) - .returns(a.ref('MyQueryReturnType')) + .returns( + a.customType({ + fieldA: a.string(), + fieldB: a.integer(), + nestedCustomType: a.customType({ + nestedA: a.string(), + nestedB: a.string(), + grandChild: a.customType({ + grandA: a.string(), + grandB: a.string(), + }), + }), + }), + ) .authorization((allow) => allow.publicApiKey()), }); @@ -1399,28 +1401,6 @@ describe('custom operations + custom type auth inheritance', () => { ); }); - test('inline custom type inherits auth rules from referencing op', () => { - const s = a.schema({ - myQuery: a - .query() - .handler(a.handler.function('myFn')) - .returns( - a.customType({ - fieldA: a.string(), - fieldB: a.integer(), - }), - ) - .authorization((allow) => allow.publicApiKey()), - }); - - const result = s.transform().schema; - - expect(result).toMatchSnapshot(); - expect(result).toEqual( - expect.stringContaining('type MyQueryReturnType @aws_api_key\n{'), - ); - }); - test('top-level custom type with nested top-level custom types inherits combined auth rules from referencing ops', () => { const s = a.schema({ myQuery: a diff --git a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap index 5832064c1..16eda2702 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap @@ -545,18 +545,6 @@ type Query { }" `; -exports[`custom operations + custom type auth inheritance inline custom type inherits auth rules from referencing op 1`] = ` -"type MyQueryReturnType @aws_api_key -{ - fieldA: String - fieldB: Int -} - -type Query { - myQuery: MyQueryReturnType @function(name: "myFn") @auth(rules: [{allow: public, provider: apiKey}]) -}" -`; - exports[`custom operations + custom type auth inheritance nested custom types inherit auth rules from top-level referencing op 1`] = ` "type MyQueryReturnType @aws_api_key { diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index bb710d6d3..68662b6ea 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -1424,7 +1424,7 @@ function generateInputTypes( generateInputType, getRefType, authRules, - true, + false, ); nestedGeneratedTypes.forEach((type) => { generatedTypes.add(type); From 6b82830fb4c8356f87d503102db51ca13389b606 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Tue, 21 Jan 2025 18:13:02 -0800 Subject: [PATCH 10/12] Add unit tests for custom operations with custom types and refs in CustomOperations --- .../__tests__/CustomOperations.test.ts | 78 ++++++++++++++++++ .../CustomOperations.test.ts.snap | 64 +++++++++++++++ .../__snapshots__/custom-operations.ts.snap | 80 ------------------ .../2-expected-use/custom-operations.ts | 81 ------------------- 4 files changed, 142 insertions(+), 161 deletions(-) diff --git a/packages/data-schema/__tests__/CustomOperations.test.ts b/packages/data-schema/__tests__/CustomOperations.test.ts index ca9820776..1feb9240e 100644 --- a/packages/data-schema/__tests__/CustomOperations.test.ts +++ b/packages/data-schema/__tests__/CustomOperations.test.ts @@ -1089,6 +1089,84 @@ describe('CustomOperation transform', () => { }); }); + describe('custom operations with custom types and refs', () => { + test('Schema with custom query using custom type argument', () => { + const s = a + .schema({ + CustomArgType: a.customType({ + field: a.string(), + }), + queryWithCustomTypeArg: a + .query() + .arguments({ customArg: a.ref('CustomArgType') }) + .returns(a.string()) + .handler(a.handler.function('myFunc')), + }) + .authorization((allow) => allow.publicApiKey()); + + const result = s.transform().schema; + + expect(result).toMatchSnapshot(); + }); + + test('Schema with custom query using ref argument', () => { + const s = a + .schema({ + RefArgType: a.customType({ + field: a.string(), + }), + queryWithRefArg: a + .query() + .arguments({ refArg: a.ref('RefArgType') }) + .returns(a.string()) + .handler(a.handler.function('myFunc')), + }) + .authorization((allow) => allow.publicApiKey()); + + const result = s.transform().schema; + + expect(result).toMatchSnapshot(); + }); + + test('Schema with custom mutation using custom type argument', () => { + const s = a + .schema({ + CustomArgType: a.customType({ + field: a.string(), + }), + mutateWithCustomTypeArg: a + .mutation() + .arguments({ customArg: a.ref('CustomArgType') }) + .returns(a.string()) + .handler(a.handler.function('myFunc')), + }) + .authorization((allow) => allow.publicApiKey()); + + const result = s.transform().schema; + + expect(result).toMatchSnapshot(); + }); + + test('Schema with custom mutation using ref argument', () => { + const s = a + .schema({ + RefArgType: a.customType({ + field: a.string(), + }), + mutationWithRefArg: a + .mutation() + .arguments({ refArg: a.ref('RefArgType') }) + .returns(a.string()) + .handler(a.handler.function('myFunc')), + }) + .authorization((allow) => allow.publicApiKey()); + + const result = s.transform().schema; + + expect(result).toMatchSnapshot(); + }); + }); + const fakeSecret = () => ({}) as any; const datasourceConfigMySQL = { diff --git a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap index 16eda2702..adbc40416 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap @@ -1,5 +1,69 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`CustomOperation transform custom operations with custom types and refs Schema with custom mutation using custom type argument 1`] = ` +"type CustomArgType +{ + field: String +} + +type Mutation { + mutateWithCustomTypeArg(customArg: MutateWithCustomTypeArgCustomArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) +} + +input MutateWithCustomTypeArgCustomArgInput +{ + field: String +}" +`; + +exports[`CustomOperation transform custom operations with custom types and refs Schema with custom mutation using ref argument 1`] = ` +"type RefArgType +{ + field: String +} + +type Mutation { + mutationWithRefArg(refArg: MutationWithRefArgRefArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) +} + +input MutationWithRefArgRefArgInput +{ + field: String +}" +`; + +exports[`CustomOperation transform custom operations with custom types and refs Schema with custom query using custom type argument 1`] = ` +"type CustomArgType +{ + field: String +} + +type Query { + queryWithCustomTypeArg(customArg: QueryWithCustomTypeArgCustomArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) +} + +input QueryWithCustomTypeArgCustomArgInput +{ + field: String +}" +`; + +exports[`CustomOperation transform custom operations with custom types and refs Schema with custom query using ref argument 1`] = ` +"type RefArgType +{ + field: String +} + +type Query { + queryWithRefArg(refArg: QueryWithRefArgRefArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) +} + +input QueryWithRefArgRefArgInput +{ + field: String +}" +`; + exports[`CustomOperation transform dynamo schema Custom Mutation w required arg and enum 1`] = ` "type Post @model @auth(rules: [{allow: private}]) { diff --git a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap index 270c95500..d9625d55f 100644 --- a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap +++ b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/__snapshots__/custom-operations.ts.snap @@ -40,86 +40,6 @@ exports[`custom operations async sync 1`] = ` ] `; -exports[`custom operations client operations with custom types and refs client can call custom mutation with custom type argument 1`] = ` -[ - [ - { - "authMode": undefined, - "authToken": undefined, - "query": " - mutation($customArg: MutateWithCustomTypeArgCustomArgInput) { - mutateWithCustomTypeArg(customArg: $customArg) - } - ", - "variables": { - "customArg": {}, - }, - }, - {}, - ], -] -`; - -exports[`custom operations client operations with custom types and refs client can call custom mutation with ref argument 1`] = ` -[ - [ - { - "authMode": undefined, - "authToken": undefined, - "query": " - mutation($refArg: MutationWithRefArgRefArgInput) { - mutationWithRefArg(refArg: $refArg) - } - ", - "variables": { - "refArg": {}, - }, - }, - {}, - ], -] -`; - -exports[`custom operations client operations with custom types and refs client can call custom query with custom type argument 1`] = ` -[ - [ - { - "authMode": undefined, - "authToken": undefined, - "query": " - query($customArg: QueryWithCustomTypeArgCustomArgInput) { - queryWithCustomTypeArg(customArg: $customArg) - } - ", - "variables": { - "customArg": {}, - }, - }, - {}, - ], -] -`; - -exports[`custom operations client operations with custom types and refs client can call custom query with ref argument 1`] = ` -[ - [ - { - "authMode": undefined, - "authToken": undefined, - "query": " - query($refArg: QueryWithRefArgRefArgInput) { - queryWithRefArg(refArg: $refArg) - } - ", - "variables": { - "refArg": {}, - }, - }, - {}, - ], -] -`; - exports[`custom operations custom type array result 1`] = ` [ [ diff --git a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts index 4a00b7f05..f86c03fdb 100644 --- a/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts +++ b/packages/integration-tests/__tests__/defined-behavior/2-expected-use/custom-operations.ts @@ -729,85 +729,4 @@ describe('custom operations', () => { const { data } = await client.queries.echoEnum({ status: 'BAD VALUE' }); }); }); - describe('client operations with custom types and refs', () => { - test('client can call custom query with custom type argument', async () => { - const { spy, generateClient } = mockedGenerateClient([ - { - data: { - queryWithCustomTypeArg: 'Custom type query result', - }, - }, - ]); - - const config = await buildAmplifyConfig(schema); - Amplify.configure(config); - const client = generateClient(); - - const { data } = await client.queries.queryWithCustomTypeArg({ - customArg: {}, - }); - expect(data).toEqual('Custom type query result'); - expect(optionsAndHeaders(spy)).toMatchSnapshot(); - }); - - test('client can call custom query with ref argument', async () => { - const { spy, generateClient } = mockedGenerateClient([ - { - data: { - queryWithRefArg: 'Ref type query result', - }, - }, - ]); - - const config = await buildAmplifyConfig(schema); - Amplify.configure(config); - const client = generateClient(); - - const { data } = await client.queries.queryWithRefArg({ - refArg: {}, - }); - expect(data).toEqual('Ref type query result'); - expect(optionsAndHeaders(spy)).toMatchSnapshot(); - }); - - test('client can call custom mutation with custom type argument', async () => { - const { spy, generateClient } = mockedGenerateClient([ - { - data: { - mutateWithCustomTypeArg: 'Custom type mutation result', - }, - }, - ]); - - const config = await buildAmplifyConfig(schema); - Amplify.configure(config); - const client = generateClient(); - - const { data } = await client.mutations.mutateWithCustomTypeArg({ - customArg: {}, - }); - expect(data).toEqual('Custom type mutation result'); - expect(optionsAndHeaders(spy)).toMatchSnapshot(); - }); - - test('client can call custom mutation with ref argument', async () => { - const { spy, generateClient } = mockedGenerateClient([ - { - data: { - mutationWithRefArg: 'Ref type mutation result', - }, - }, - ]); - - const config = await buildAmplifyConfig(schema); - Amplify.configure(config); - const client = generateClient(); - - const { data } = await client.mutations.mutationWithRefArg({ - refArg: {}, - }); - expect(data).toEqual('Ref type mutation result'); - expect(optionsAndHeaders(spy)).toMatchSnapshot(); - }); - }); }); From f1b8e0e546f5fba9657df053eb6d061475e3de7b Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Wed, 22 Jan 2025 18:41:50 -0800 Subject: [PATCH 11/12] Refactor schemaPreprocessor Consolidate schema components into single array --- .../__snapshots__/ClientSchema.test.ts.snap | 12 ++--- .../CustomOperations.test.ts.snap | 48 +++++++++--------- .../__snapshots__/CustomType.test.ts.snap | 50 +++++++++---------- packages/data-schema/src/SchemaProcessor.ts | 43 +++++----------- 4 files changed, 68 insertions(+), 85 deletions(-) diff --git a/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap index 2273c0366..97e4d41a1 100644 --- a/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/ClientSchema.test.ts.snap @@ -250,12 +250,6 @@ exports[`custom operations Add entities to SQL schema add custom type, enum, and author: String } -type PostMeta @aws_cognito_user_pools -{ - viewCount: Int - approvedOn: AWSDate -} - enum PostStatus { draft pending @@ -263,6 +257,12 @@ enum PostStatus { published } +type PostMeta @aws_cognito_user_pools +{ + viewCount: Int + approvedOn: AWSDate +} + type Query { getPostMeta(id: String): PostMeta @sql(statement: "SELECT viewCount, approvedOn FROM some_table") @auth(rules: [{allow: private}]) }" diff --git a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap index adbc40416..bc828f26d 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomOperations.test.ts.snap @@ -1,66 +1,66 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CustomOperation transform custom operations with custom types and refs Schema with custom mutation using custom type argument 1`] = ` -"type CustomArgType +"input MutateWithCustomTypeArgCustomArgInput { field: String } -type Mutation { - mutateWithCustomTypeArg(customArg: MutateWithCustomTypeArgCustomArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) -} - -input MutateWithCustomTypeArgCustomArgInput +type CustomArgType { field: String +} + +type Mutation { + mutateWithCustomTypeArg(customArg: MutateWithCustomTypeArgCustomArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) }" `; exports[`CustomOperation transform custom operations with custom types and refs Schema with custom mutation using ref argument 1`] = ` -"type RefArgType +"input MutationWithRefArgRefArgInput { field: String } -type Mutation { - mutationWithRefArg(refArg: MutationWithRefArgRefArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) -} - -input MutationWithRefArgRefArgInput +type RefArgType { field: String +} + +type Mutation { + mutationWithRefArg(refArg: MutationWithRefArgRefArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) }" `; exports[`CustomOperation transform custom operations with custom types and refs Schema with custom query using custom type argument 1`] = ` -"type CustomArgType +"input QueryWithCustomTypeArgCustomArgInput { field: String } -type Query { - queryWithCustomTypeArg(customArg: QueryWithCustomTypeArgCustomArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) -} - -input QueryWithCustomTypeArgCustomArgInput +type CustomArgType { field: String +} + +type Query { + queryWithCustomTypeArg(customArg: QueryWithCustomTypeArgCustomArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) }" `; exports[`CustomOperation transform custom operations with custom types and refs Schema with custom query using ref argument 1`] = ` -"type RefArgType +"input QueryWithRefArgRefArgInput { field: String } -type Query { - queryWithRefArg(refArg: QueryWithRefArgRefArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) -} - -input QueryWithRefArgRefArgInput +type RefArgType { field: String +} + +type Query { + queryWithRefArg(refArg: QueryWithRefArgRefArgInput): String @function(name: "myFunc") @auth(rules: [{allow: public, provider: apiKey}]) }" `; diff --git a/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap b/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap index a396d9323..3b1a10820 100644 --- a/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap +++ b/packages/data-schema/__tests__/__snapshots__/CustomType.test.ts.snap @@ -45,6 +45,11 @@ exports[`CustomType transform Explicit CustomType nests explicit CustomType 1`] meta: Meta } +enum PostStatus { + unpublished + published +} + type Meta { status: PostStatus @@ -54,11 +59,6 @@ type Meta type AltMeta { field1: String -} - -enum PostStatus { - unpublished - published }" `; @@ -68,15 +68,15 @@ exports[`CustomType transform Explicit CustomType nests explicit enum 1`] = ` meta: Meta } +enum PostStatus { + unpublished + published +} + type Meta { status: PostStatus publishedDate: AWSDate -} - -enum PostStatus { - unpublished - published }" `; @@ -92,14 +92,14 @@ type Meta nestedMeta: MetaNestedMeta } -type MetaNestedMeta -{ - field1: String -} - enum MetaStatus { unpublished published +} + +type MetaNestedMeta +{ + field1: String }" `; @@ -163,15 +163,15 @@ exports[`CustomType transform Implicit CustomType nests explicit enum 1`] = ` meta: PostMeta } +enum PostStatus { + unpublished + published +} + type PostMeta { status: PostStatus publishedDate: AWSDate -} - -enum PostStatus { - unpublished - published }" `; @@ -187,14 +187,14 @@ type PostMeta nestedMeta: PostMetaNestedMeta } -type PostMetaNestedMeta -{ - field1: String -} - enum PostMetaStatus { unpublished published +} + +type PostMetaNestedMeta +{ + field1: String }" `; diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index 68662b6ea..01a4079d2 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -361,7 +361,6 @@ function customOperationToGql( let callSignature: string = typeName; const implicitTypes: [string, any][] = []; const inputTypes: string[] = []; - let returnTypes: string[] = []; // When Custom Operations are defined with a Custom Type return type, @@ -1432,10 +1431,8 @@ function generateInputTypes( } } else if (typeDef.type === 'enum') { const enumDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; - generatedTypes.add(enumDefinition); } else if (typeDef.type === 'scalar') { - const scalarDefinition = `scalar ${typeName}`; - generatedTypes.add(scalarDefinition); + generatedTypes.add(`scalar ${typeName}`); } else { console.warn(`Unexpected type definition for ${typeName}:`, typeDef); } @@ -1453,10 +1450,7 @@ const schemaPreprocessor = ( lambdaFunctions: LambdaFunctionDefinition; customSqlDataSourceStrategies?: CustomSqlDataSourceStrategy[]; } => { - const enumTypes = new Set(); - const gqlModels: string[] = []; - const inputTypes = new Map(); - + const gqlComponents: string[] = []; const customQueries = []; const customMutations = []; const customSubscriptions = []; @@ -1519,7 +1513,7 @@ const schemaPreprocessor = ( ); } const enumType = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; - enumTypes.add(enumType); + gqlComponents.push(enumType); } else if (isCustomType(typeDef)) { const fields = typeDef.data.fields; @@ -1570,7 +1564,7 @@ const schemaPreprocessor = ( const joined = gqlFields.join('\n '); const model = `type ${typeName} ${customAuth}\n{\n ${joined}\n}`; - gqlModels.push(model); + gqlComponents.push(model); } else if (isCustomOperation(typeDef)) { // TODO: add generation route logic. @@ -1593,10 +1587,9 @@ const schemaPreprocessor = ( getRefType, ); operationInputTypes.forEach((type) => { - const typeName = type.split(' ')[1]; - inputTypes.set(typeName, type); + gqlComponents.push(type); }); - gqlModels.push(...operationReturnTypes); + gqlComponents.push(...operationReturnTypes); /** * Processes implicit types to generate GraphQL definitions. @@ -1613,7 +1606,7 @@ const schemaPreprocessor = ( implicitTypes.forEach(([typeName, typeDef]) => { if (isEnumType(typeDef)) { const enumTypeDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; - enumTypes.add(enumTypeDefinition); + gqlComponents.push(enumTypeDefinition); } else { const shouldGenerateInputType = !operationReturnTypes.some( (returnType) => returnType.includes(typeName), @@ -1624,10 +1617,7 @@ const schemaPreprocessor = ( getRefType, ); if (shouldGenerateInputType) { - generatedTypeDefinitions.forEach((def) => { - const defName = def.split(' ')[1]; - inputTypes.set(defName, def); - }); + gqlComponents.push(...generatedTypeDefinitions); } } }); @@ -1736,7 +1726,7 @@ const schemaPreprocessor = ( // passing (timestamps: null) to @model to suppress this behavior as a short // term solution. const model = `type ${typeName} @model(timestamps: null) ${authString}${refersToString}\n{\n ${joined}\n}`; - gqlModels.push(model); + gqlComponents.push(model); } else { const fields = typeDef.data.fields as Record; @@ -1799,7 +1789,7 @@ const schemaPreprocessor = ( const modelDirective = modelAttrs ? `@model(${modelAttrs})` : '@model'; const model = `type ${typeName} ${modelDirective} ${authString}\n{\n ${joined}\n}`; - gqlModels.push(model); + gqlComponents.push(model); } } @@ -1809,19 +1799,12 @@ const schemaPreprocessor = ( subscriptions: customSubscriptions, }; - const customOperationTypes = generateCustomOperationTypes(customOperations); - - const schemaComponents = [ - ...gqlModels, - ...Array.from(enumTypes), - ...customOperationTypes, - ...Array.from(inputTypes.values()), - ]; + gqlComponents.push(...generateCustomOperationTypes(customOperations)); if (shouldAddConversationTypes) { - schemaComponents.push(CONVERSATION_SCHEMA_GRAPHQL_TYPES); + gqlComponents.push(CONVERSATION_SCHEMA_GRAPHQL_TYPES); } - const processedSchema = schemaComponents.join('\n\n'); + const processedSchema = gqlComponents.join('\n\n'); return { schema: processedSchema, From acaa7d4e751de370a13738d5726d1a5531a1ded2 Mon Sep 17 00:00:00 2001 From: Vaisshnavi Date: Wed, 22 Jan 2025 22:35:33 -0800 Subject: [PATCH 12/12] Improve tests for CustomType and RefType arguments in custom operations --- .../__tests__/CustomOperations.test-d.ts | 16 ++++++++--- packages/data-schema/src/SchemaProcessor.ts | 27 +++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/data-schema/__tests__/CustomOperations.test-d.ts b/packages/data-schema/__tests__/CustomOperations.test-d.ts index 455c167d8..f81306d9a 100644 --- a/packages/data-schema/__tests__/CustomOperations.test-d.ts +++ b/packages/data-schema/__tests__/CustomOperations.test-d.ts @@ -860,27 +860,35 @@ describe('.for() modifier', () => { describe('.arguments() modifier', () => { // Test to verify that CustomType can be used as an argument in custom operations it('accepts CustomType in arguments', () => { - const operation: CustomOperation< + type ExpectedType = CustomOperation< any, 'arguments' | 'for', 'queryCustomOperation' - > = a.query().arguments({ + >; + + const operation = a.query().arguments({ customArg: a.customType({ field1: a.string(), field2: a.integer(), }), }); + + type test = Expect>; }); // Test to verify that RefType can be used as an argument in custom operations it('accepts RefType in arguments', () => { - const operation: CustomOperation< + type ExpectedType = CustomOperation< any, 'arguments' | 'for', 'queryCustomOperation' - > = a.query().arguments({ + >; + + const operation = a.query().arguments({ refArg: a.ref('SomeType'), }); + + type test = Expect>; }); it('handles deeply nested custom types', () => { diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index 01a4079d2..297e2d2f1 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -1036,7 +1036,7 @@ function processFields( if (fieldName === partitionKey) { gqlFields.push( `${fieldName}: ${scalarFieldToGql( - fieldDef.data, + (fieldDef as any).data, identifier, secondaryIndexes[fieldName], )}${fieldAuth}`, @@ -1429,8 +1429,6 @@ function generateInputTypes( generatedTypes.add(type); }); } - } else if (typeDef.type === 'enum') { - const enumDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; } else if (typeDef.type === 'scalar') { generatedTypes.add(`scalar ${typeName}`); } else { @@ -1605,19 +1603,20 @@ const schemaPreprocessor = ( implicitTypes.forEach(([typeName, typeDef]) => { if (isEnumType(typeDef)) { - const enumTypeDefinition = `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`; - gqlComponents.push(enumTypeDefinition); - } else { - const shouldGenerateInputType = !operationReturnTypes.some( - (returnType) => returnType.includes(typeName), + gqlComponents.push( + `enum ${typeName} {\n ${typeDef.values.join('\n ')}\n}`, ); - const generatedTypeDefinitions = generateInputTypes( - [[typeName, typeDef]], - shouldGenerateInputType, - getRefType, + } else { + const isReturnType = operationReturnTypes.some((returnType) => + returnType.includes(typeName), ); - if (shouldGenerateInputType) { - gqlComponents.push(...generatedTypeDefinitions); + if (!isReturnType) { + const generatedTypes = generateInputTypes( + [[typeName, typeDef]], + true, + getRefType, + ); + gqlComponents.push(...generatedTypes); } } });