diff --git a/.codebuild/e2e_workflow.yml b/.codebuild/e2e_workflow.yml index b250e831..39a3c8ea 100644 --- a/.codebuild/e2e_workflow.yml +++ b/.codebuild/e2e_workflow.yml @@ -145,24 +145,25 @@ batch: depend-on: - publish_to_local_registry - identifier: >- - l_build_app_ts_uninitialized_project_codegen_js_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter + l_build_app_ts_push_codegen_admin_modelgen_uninitialized_project_codegen_js_uninitialized_project_modelgen_android buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: TEST_SUITE: >- - src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts + src/__tests__/build-app-ts.test.ts|src/__tests__/push-codegen-admin-modelgen.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts CLI_REGION: ap-southeast-1 DISABLE_ESLINT_PLUGIN: true depend-on: - publish_to_local_registry - - identifier: l_uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js + - identifier: >- + l_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: TEST_SUITE: >- - src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts + src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts CLI_REGION: ap-southeast-2 depend-on: - publish_to_local_registry @@ -251,7 +252,7 @@ batch: - publish_to_local_registry - build_windows - identifier: >- - w_build_app_ts_uninitialized_project_codegen_js_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter + w_build_app_ts_push_codegen_admin_modelgen_uninitialized_project_codegen_js_uninitialized_project_modelgen_android buildspec: .codebuild/run_e2e_tests_windows.yml env: compute-type: BUILD_GENERAL1_LARGE @@ -259,13 +260,14 @@ batch: type: WINDOWS_SERVER_2019_CONTAINER variables: TEST_SUITE: >- - src/__tests__/build-app-ts.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts|src/__tests__/uninitialized-project-modelgen-flutter.test.ts + src/__tests__/build-app-ts.test.ts|src/__tests__/push-codegen-admin-modelgen.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts CLI_REGION: us-east-1 DISABLE_ESLINT_PLUGIN: true depend-on: - publish_to_local_registry - build_windows - - identifier: w_uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js + - identifier: >- + w_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js buildspec: .codebuild/run_e2e_tests_windows.yml env: compute-type: BUILD_GENERAL1_LARGE @@ -273,7 +275,7 @@ batch: type: WINDOWS_SERVER_2019_CONTAINER variables: TEST_SUITE: >- - src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts + src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts CLI_REGION: us-east-1 depend-on: - publish_to_local_registry diff --git a/packages/amplify-codegen-e2e-core/src/utils/sdk-calls.ts b/packages/amplify-codegen-e2e-core/src/utils/sdk-calls.ts index 4987ec61..f132944b 100644 --- a/packages/amplify-codegen-e2e-core/src/utils/sdk-calls.ts +++ b/packages/amplify-codegen-e2e-core/src/utils/sdk-calls.ts @@ -14,6 +14,7 @@ import { AmplifyBackend, } from 'aws-sdk'; import _ from 'lodash'; +import { getProjectMeta } from './projectMeta'; export const getDDBTable = async (tableName: string, region: string) => { const service = new DynamoDB({ region }); @@ -42,6 +43,19 @@ export const bucketNotExists = async (bucket: string) => { } }; +export const getDeploymentBucketObject = async (projectRoot: string, objectKey: string) => { + const meta = getProjectMeta(projectRoot); + const deploymentBucket = meta.providers.awscloudformation.DeploymentBucketName; + const s3 = new S3(); + const result = await s3 + .getObject({ + Bucket: deploymentBucket, + Key: objectKey, + }) + .promise(); + return result.Body.toLocaleString(); +}; + export const deleteS3Bucket = async (bucket: string, providedS3Client: S3 | undefined = undefined) => { const s3 = providedS3Client ? providedS3Client : new S3(); let continuationToken: Required> = undefined; diff --git a/packages/amplify-codegen-e2e-tests/schemas/admin-modelgen.graphql b/packages/amplify-codegen-e2e-tests/schemas/admin-modelgen.graphql new file mode 100644 index 00000000..a2ecffa1 --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/schemas/admin-modelgen.graphql @@ -0,0 +1,53 @@ +input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY! + +type Todo @model { + id: ID! + name: String! + description: String + phone: Phone +} +type Phone { + number: String +} +enum BillingSource { + CLIENT + PROJECT +} +input CustomInput { + customField1: String! + customField2: BillingSource + customField3: NestedInput! +} +input NestedInput { + content: String! = "hello" +} +interface ICustom { + firstName: String! + lastName: String + birthdays: [INestedCustom!]! +} +interface INestedCustom { + birthDay: AWSDate! +} +# The member types of a Union type must all be Object base types. +union CustomUnion = Todo | Phone + +type Query { + getAllTodo(msg: String, input: CustomInput): String @function(name: "echofunction-${env}") + echo(msg: String!): String + echo2(todoId: ID!): Todo + echo3: [Todo!]! + echo4(number: String): Phone + echo5: [CustomUnion!]! + echo6(customInput: CustomInput): String! + echo7: [ICustom]! + echo8(msg: [Float], msg2: [Int!], enumType: BillingSource, enumList: [BillingSource], inputType: [CustomInput]): [String] + echo9(msg: [Float]!, msg2: [Int!]!, enumType: BillingSource!, enumList: [BillingSource!]!, inputType: [CustomInput!]!): [String!]! + +} +type Mutation { + mutate(msg: [String!]!): Todo +} +type Subscription { + onMutate(msg: String): [Todo!] +} \ No newline at end of file diff --git a/packages/amplify-codegen-e2e-tests/src/__tests__/push-codegen-admin-modelgen.test.ts b/packages/amplify-codegen-e2e-tests/src/__tests__/push-codegen-admin-modelgen.test.ts new file mode 100644 index 00000000..5c7bedb1 --- /dev/null +++ b/packages/amplify-codegen-e2e-tests/src/__tests__/push-codegen-admin-modelgen.test.ts @@ -0,0 +1,19 @@ +import { DEFAULT_JS_CONFIG, createNewProjectDir } from "@aws-amplify/amplify-codegen-e2e-core"; +import { deleteAmplifyProject, testPushAdminModelgen, testPushCodegen } from "../codegen-tests-base"; + +const schema = 'admin-modelgen.graphql'; + +describe('Amplify push with codegen tests - admin modelgen', () => { + let projectRoot: string; + beforeEach(async () => { + projectRoot = await createNewProjectDir('pushCodegenAdminModelgen'); + }); + + afterEach(async () => { + await deleteAmplifyProject(projectRoot); + }); + + it(`should not throw error for executing the admin modelgen step required by studio CMS usage post push given the schema with input, union and interface types`, async () => { + await testPushAdminModelgen(DEFAULT_JS_CONFIG, projectRoot, schema); + }); +}); \ No newline at end of file diff --git a/packages/amplify-codegen-e2e-tests/src/cleanup-e2e-resources.ts b/packages/amplify-codegen-e2e-tests/src/cleanup-e2e-resources.ts index 51d6de17..a09976c5 100644 --- a/packages/amplify-codegen-e2e-tests/src/cleanup-e2e-resources.ts +++ b/packages/amplify-codegen-e2e-tests/src/cleanup-e2e-resources.ts @@ -45,6 +45,7 @@ type AmplifyAppInfo = { type S3BucketInfo = { name: string; + region: string; jobId?: string; cbInfo?: CodeBuild.Build; }; @@ -118,7 +119,16 @@ const getOrphanS3TestBuckets = async (account: AWSAccountInfo): Promise ({ name: it.Name })); + const bucketInfos = await Promise.all( + staleBuckets.map(async (staleBucket): Promise => { + const region = await getBucketRegion(account, staleBucket.Name); + return { + name: staleBucket.Name, + region, + }; + }), + ); + return bucketInfos; }; /** @@ -276,27 +286,52 @@ const getJobCodeBuildDetails = async (jobIds: string[]): Promise => { + const awsConfig = getAWSConfig(account); + const s3Client = new aws.S3(awsConfig); + const location = await s3Client.getBucketLocation({ Bucket: bucketName }).promise(); + const region = location.LocationConstraint ?? 'us-east-1'; + return region; +}; + const getS3Buckets = async (account: AWSAccountInfo): Promise => { - const s3Client = new aws.S3(getAWSConfig(account)); + const awsConfig = getAWSConfig(account); + const s3Client = new aws.S3(awsConfig); const buckets = await s3Client.listBuckets().promise(); const result: S3BucketInfo[] = []; for (const bucket of buckets.Buckets) { + let region: string | undefined; try { - const bucketDetails = await s3Client.getBucketTagging({ Bucket: bucket.Name }).promise(); + region = await getBucketRegion(account, bucket.Name); + // Operations on buckets created in opt-in regions appear to require region-specific clients + const regionalizedClient = new aws.S3({ + region, + ...(awsConfig as object), + }); + const bucketDetails = await regionalizedClient.getBucketTagging({ Bucket: bucket.Name }).promise(); const jobId = getJobId(bucketDetails.TagSet); if (jobId) { result.push({ name: bucket.Name, + region, jobId }); } } catch (e) { - if (e.code !== 'NoSuchTagSet' && e.code !== 'NoSuchBucket') { + // TODO: Why do we process the bucket even with these particular errors? + if (e.code === 'NoSuchTagSet' || e.code === 'NoSuchBucket') { + result.push({ + name: bucket.Name, + region: region ?? 'us-east-1', + }); + } else if (e.code === 'InvalidToken') { + // We see some buckets in some accounts that were somehow created in an opt-in region different from the one to which the account is + // actually opted in. We don't quite know how this happened, but for now, we'll make a note of the inconsistency and continue + // processing the rest of the buckets. + console.error(`Skipping processing ${account.accountId}, bucket ${bucket.Name}`, e); + } else { throw e; } - result.push({ - name: bucket.Name, - }); } } return result; @@ -516,8 +551,12 @@ const deleteBucket = async (account: AWSAccountInfo, accountIndex: number, bucke const { name } = bucket; try { console.log(`${generateAccountInfo(account, accountIndex)} Deleting S3 Bucket ${name}`); - const s3 = new aws.S3(getAWSConfig(account)); - await deleteS3Bucket(name, s3); + const awsConfig = getAWSConfig(account); + const regionalizedS3Client = new aws.S3({ + region: bucket.region, + ...(awsConfig as object), + }); + await deleteS3Bucket(name, regionalizedS3Client); } catch (e) { console.log(`${generateAccountInfo(account, accountIndex)} Deleting bucket ${name} failed with error ${e.message}`); if (e.code === 'ExpiredTokenException') { diff --git a/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/push-codegen.ts b/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/push-codegen.ts index d5315833..20e65dc7 100644 --- a/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/push-codegen.ts +++ b/packages/amplify-codegen-e2e-tests/src/codegen-tests-base/push-codegen.ts @@ -6,7 +6,10 @@ import { amplifyPushWithCodegenAdd, AmplifyFrontendConfig, amplifyPushWithCodegenUpdate, - updateAPIWithResolutionStrategyWithModels + updateAPIWithResolutionStrategyWithModels, + getProjectMeta, + getDeploymentBucketObject, + amplifyPush } from "@aws-amplify/amplify-codegen-e2e-core"; import { existsSync } from "fs"; import path from 'path'; @@ -38,3 +41,38 @@ export async function testPushCodegen(config: AmplifyFrontendConfig, projectRoot expect(existsSync(userSourceCodePath)).toBe(true); expect(isNotEmptyDir(path.join(projectRoot, config.modelgenDir))).toBe(true); } + +export async function testPushAdminModelgen(config: AmplifyFrontendConfig, projectRoot: string, schema: string) { + // init project and add API category + await initProjectWithProfile(projectRoot, { ...config, disableAmplifyAppCreation: false, }); + const { + DeploymentBucketName: bucketName, + Region: region, + AmplifyAppId: appId, + } = getProjectMeta(projectRoot).providers.awscloudformation; + + expect(bucketName).toBeDefined() + expect(region).toBeDefined(); + expect(appId).toBeDefined(); + + const projectName = createRandomName(); + await addApiWithoutSchema(projectRoot, { apiName: projectName }); + await updateApiSchema(projectRoot, projectName, schema); + // add codegen succeeds + await amplifyPush(projectRoot); + + /** + * Source code from + * https://github.com/aws-amplify/amplify-cli/blob/1da5de70c57b15a76f02c92364af4889d1585229/packages/amplify-provider-awscloudformation/src/admin-modelgen.ts#L85-L93 + */ + const s3ApiModelsPrefix = `models/${projectName}/`; + const cmsArtifactLocalToS3Keys = [ + `${s3ApiModelsPrefix}schema.graphql`, + `${s3ApiModelsPrefix}schema.js`, + `${s3ApiModelsPrefix}modelIntrospection.json`, + ]; + // expect CMS assets to be present in S3 + cmsArtifactLocalToS3Keys.forEach(async (key) => { + await expect(getDeploymentBucketObject(projectRoot, key)).resolves.not.toThrow(); + }); +} diff --git a/packages/appsync-modelgen-plugin/API.md b/packages/appsync-modelgen-plugin/API.md index 803cb2dc..f9b62d48 100644 --- a/packages/appsync-modelgen-plugin/API.md +++ b/packages/appsync-modelgen-plugin/API.md @@ -28,7 +28,7 @@ export interface AppSyncModelPluginConfig extends RawDocumentsConfig { // @public (undocumented) export type Argument = { name: string; - type: FieldType; + type: InputFieldType; isArray: boolean; isRequired: boolean; isArrayNullable?: boolean; @@ -94,7 +94,7 @@ export type FieldAttribute = ModelAttribute; export type Fields = Record; // @public (undocumented) -export type FieldType = 'ID' | 'String' | 'Int' | 'Float' | 'AWSDate' | 'AWSTime' | 'AWSDateTime' | 'AWSTimestamp' | 'AWSEmail' | 'AWSURL' | 'AWSIPAddress' | 'Boolean' | 'AWSJSON' | 'AWSPhone' | { +export type FieldType = ScalarType | { enum: string; } | { model: string; @@ -102,6 +102,19 @@ export type FieldType = 'ID' | 'String' | 'Int' | 'Float' | 'AWSDate' | 'AWSTime nonModel: string; }; +// @public (undocumented) +export type Input = { + name: string; + attributes: Arguments; +}; + +// @public (undocumented) +export type InputFieldType = ScalarType | { + enum: string; +} | { + input: string; +}; + // @public (undocumented) export type ModelAttribute = { type: string; @@ -119,6 +132,7 @@ export type ModelIntrospectionSchema = { queries?: SchemaQueries; mutations?: SchemaMutations; subscriptions?: SchemaSubscriptions; + inputs?: SchemaInputs; }; // Warning: (ae-forgotten-export) The symbol "RawAppSyncModelConfig" needs to be exported by the entry point index.d.ts @@ -136,6 +150,9 @@ export type PrimaryKeyInfo = { sortKeyFieldNames: string[]; }; +// @public (undocumented) +export type ScalarType = 'ID' | 'String' | 'Int' | 'Float' | 'AWSDate' | 'AWSTime' | 'AWSDateTime' | 'AWSTimestamp' | 'AWSEmail' | 'AWSURL' | 'AWSIPAddress' | 'Boolean' | 'AWSJSON' | 'AWSPhone'; + // @public (undocumented) export type SchemaEnum = { name: string; @@ -145,6 +162,9 @@ export type SchemaEnum = { // @public (undocumented) export type SchemaEnums = Record; +// @public (undocumented) +export type SchemaInputs = Record; + // @public (undocumented) export type SchemaModel = { name: string; diff --git a/packages/appsync-modelgen-plugin/schemas/introspection/1/ModelIntrospectionSchema.json b/packages/appsync-modelgen-plugin/schemas/introspection/1/ModelIntrospectionSchema.json index bc842831..d92926be 100644 --- a/packages/appsync-modelgen-plugin/schemas/introspection/1/ModelIntrospectionSchema.json +++ b/packages/appsync-modelgen-plugin/schemas/introspection/1/ModelIntrospectionSchema.json @@ -23,6 +23,9 @@ }, "subscriptions": { "$ref": "#/definitions/SchemaSubscriptions" + }, + "inputs": { + "$ref": "#/definitions/SchemaInputs" } }, "required": [ @@ -147,60 +150,7 @@ "FieldType": { "anyOf": [ { - "type": "string", - "const": "ID" - }, - { - "type": "string", - "const": "String" - }, - { - "type": "string", - "const": "Int" - }, - { - "type": "string", - "const": "Float" - }, - { - "type": "string", - "const": "AWSDate" - }, - { - "type": "string", - "const": "AWSTime" - }, - { - "type": "string", - "const": "AWSDateTime" - }, - { - "type": "string", - "const": "AWSTimestamp" - }, - { - "type": "string", - "const": "AWSEmail" - }, - { - "type": "string", - "const": "AWSURL" - }, - { - "type": "string", - "const": "AWSIPAddress" - }, - { - "type": "string", - "const": "Boolean" - }, - { - "type": "string", - "const": "AWSJSON" - }, - { - "type": "string", - "const": "AWSPhone" + "$ref": "#/definitions/ScalarType" }, { "type": "object", @@ -240,6 +190,25 @@ } ] }, + "ScalarType": { + "type": "string", + "enum": [ + "ID", + "String", + "Int", + "Float", + "AWSDate", + "AWSTime", + "AWSDateTime", + "AWSTimestamp", + "AWSEmail", + "AWSURL", + "AWSIPAddress", + "Boolean", + "AWSJSON", + "AWSPhone" + ] + }, "FieldAttribute": { "$ref": "#/definitions/ModelAttribute" }, @@ -345,7 +314,7 @@ "type": "string" }, "type": { - "$ref": "#/definitions/FieldType" + "$ref": "#/definitions/InputFieldType" }, "isArray": { "type": "boolean" @@ -365,6 +334,37 @@ ], "additionalProperties": false }, + "InputFieldType": { + "anyOf": [ + { + "$ref": "#/definitions/ScalarType" + }, + { + "type": "object", + "properties": { + "enum": { + "type": "string" + } + }, + "required": [ + "enum" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "input": { + "type": "string" + } + }, + "required": [ + "input" + ], + "additionalProperties": false + } + ] + }, "PrimaryKeyInfo": { "type": "object", "properties": { @@ -506,6 +506,32 @@ }, "SchemaSubscription": { "$ref": "#/definitions/SchemaQuery" + }, + "SchemaInputs": { + "$ref": "#/definitions/Record%3Cstring%2CInput%3E" + }, + "Record": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Input" + } + }, + "Input": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "attributes": { + "$ref": "#/definitions/Arguments" + } + }, + "required": [ + "name", + "attributes" + ], + "additionalProperties": false, + "description": "Input Definition" } } } diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-model-introspection-visitor.test.ts.snap b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-model-introspection-visitor.test.ts.snap index b94c5399..e7fd2efe 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-model-introspection-visitor.test.ts.snap +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-model-introspection-visitor.test.ts.snap @@ -1612,7 +1612,7 @@ exports[`Custom primary key tests should generate correct model intropection fil }" `; -exports[`Custom queries/mutations/subscriptions tests should generate correct metadata for custom queries/mutations/subscriptions in model introspection schema 1`] = ` +exports[`Custom queries/mutations/subscriptions & input type tests should generate correct metadata for custom queries/mutations/subscriptions in model introspection schema 1`] = ` "{ \\"version\\": 1, \\"models\\": { @@ -1640,6 +1640,15 @@ exports[`Custom queries/mutations/subscriptions tests should generate correct me \\"isRequired\\": false, \\"attributes\\": [] }, + \\"phone\\": { + \\"name\\": \\"phone\\", + \\"isArray\\": false, + \\"type\\": { + \\"nonModel\\": \\"Phone\\" + }, + \\"isRequired\\": false, + \\"attributes\\": [] + }, \\"createdAt\\": { \\"name\\": \\"createdAt\\", \\"isArray\\": false, @@ -1672,7 +1681,15 @@ exports[`Custom queries/mutations/subscriptions tests should generate correct me } } }, - \\"enums\\": {}, + \\"enums\\": { + \\"BillingSource\\": { + \\"name\\": \\"BillingSource\\", + \\"values\\": [ + \\"CLIENT\\", + \\"PROJECT\\" + ] + } + }, \\"nonModels\\": { \\"Phone\\": { \\"name\\": \\"Phone\\", @@ -1688,6 +1705,28 @@ exports[`Custom queries/mutations/subscriptions tests should generate correct me } }, \\"queries\\": { + \\"getAllTodo\\": { + \\"name\\": \\"getAllTodo\\", + \\"isArray\\": false, + \\"type\\": \\"String\\", + \\"isRequired\\": false, + \\"arguments\\": { + \\"msg\\": { + \\"name\\": \\"msg\\", + \\"isArray\\": false, + \\"type\\": \\"String\\", + \\"isRequired\\": false + }, + \\"input\\": { + \\"name\\": \\"input\\", + \\"isArray\\": false, + \\"type\\": { + \\"input\\": \\"CustomInput\\" + }, + \\"isRequired\\": false + } + } + }, \\"echo\\": { \\"name\\": \\"echo\\", \\"isArray\\": false, @@ -1724,8 +1763,8 @@ exports[`Custom queries/mutations/subscriptions tests should generate correct me \\"type\\": { \\"model\\": \\"Todo\\" }, - \\"isRequired\\": false, - \\"isArrayNullable\\": true + \\"isRequired\\": true, + \\"isArrayNullable\\": false }, \\"echo4\\": { \\"name\\": \\"echo4\\", @@ -1742,6 +1781,120 @@ exports[`Custom queries/mutations/subscriptions tests should generate correct me \\"isRequired\\": false } } + }, + \\"echo6\\": { + \\"name\\": \\"echo6\\", + \\"isArray\\": false, + \\"type\\": \\"String\\", + \\"isRequired\\": true, + \\"arguments\\": { + \\"customInput\\": { + \\"name\\": \\"customInput\\", + \\"isArray\\": false, + \\"type\\": { + \\"input\\": \\"CustomInput\\" + }, + \\"isRequired\\": false + } + } + }, + \\"echo8\\": { + \\"name\\": \\"echo8\\", + \\"isArray\\": true, + \\"type\\": \\"String\\", + \\"isRequired\\": false, + \\"isArrayNullable\\": true, + \\"arguments\\": { + \\"msg\\": { + \\"name\\": \\"msg\\", + \\"isArray\\": true, + \\"type\\": \\"Float\\", + \\"isRequired\\": false, + \\"isArrayNullable\\": true + }, + \\"msg2\\": { + \\"name\\": \\"msg2\\", + \\"isArray\\": true, + \\"type\\": \\"Int\\", + \\"isRequired\\": true, + \\"isArrayNullable\\": true + }, + \\"enumType\\": { + \\"name\\": \\"enumType\\", + \\"isArray\\": false, + \\"type\\": { + \\"enum\\": \\"BillingSource\\" + }, + \\"isRequired\\": false + }, + \\"enumList\\": { + \\"name\\": \\"enumList\\", + \\"isArray\\": true, + \\"type\\": { + \\"enum\\": \\"BillingSource\\" + }, + \\"isRequired\\": false, + \\"isArrayNullable\\": true + }, + \\"inputType\\": { + \\"name\\": \\"inputType\\", + \\"isArray\\": true, + \\"type\\": { + \\"input\\": \\"CustomInput\\" + }, + \\"isRequired\\": false, + \\"isArrayNullable\\": true + } + } + }, + \\"echo9\\": { + \\"name\\": \\"echo9\\", + \\"isArray\\": true, + \\"type\\": \\"String\\", + \\"isRequired\\": true, + \\"isArrayNullable\\": false, + \\"arguments\\": { + \\"msg\\": { + \\"name\\": \\"msg\\", + \\"isArray\\": true, + \\"type\\": \\"Float\\", + \\"isRequired\\": false, + \\"isArrayNullable\\": false + }, + \\"msg2\\": { + \\"name\\": \\"msg2\\", + \\"isArray\\": true, + \\"type\\": \\"Int\\", + \\"isRequired\\": true, + \\"isArrayNullable\\": false + }, + \\"enumType\\": { + \\"name\\": \\"enumType\\", + \\"isArray\\": false, + \\"type\\": { + \\"enum\\": \\"BillingSource\\" + }, + \\"isRequired\\": true + }, + \\"enumList\\": { + \\"name\\": \\"enumList\\", + \\"isArray\\": true, + \\"type\\": { + \\"enum\\": \\"BillingSource\\" + }, + \\"isRequired\\": true, + \\"isArrayNullable\\": false + }, + \\"inputType\\": { + \\"name\\": \\"inputType\\", + \\"isArray\\": true, + \\"type\\": { + \\"input\\": \\"CustomInput\\" + }, + \\"isRequired\\": true, + \\"isArrayNullable\\": false + } + } } }, \\"mutations\\": { @@ -1781,6 +1934,44 @@ exports[`Custom queries/mutations/subscriptions tests should generate correct me } } } + }, + \\"inputs\\": { + \\"CustomInput\\": { + \\"name\\": \\"CustomInput\\", + \\"attributes\\": { + \\"customField1\\": { + \\"name\\": \\"customField1\\", + \\"isArray\\": false, + \\"type\\": \\"String\\", + \\"isRequired\\": true + }, + \\"customField2\\": { + \\"name\\": \\"customField2\\", + \\"isArray\\": false, + \\"type\\": \\"Int\\", + \\"isRequired\\": false + }, + \\"customField3\\": { + \\"name\\": \\"customField3\\", + \\"isArray\\": false, + \\"type\\": { + \\"input\\": \\"NestedInput\\" + }, + \\"isRequired\\": true + } + } + }, + \\"NestedInput\\": { + \\"name\\": \\"NestedInput\\", + \\"attributes\\": { + \\"content\\": { + \\"name\\": \\"content\\", + \\"isArray\\": false, + \\"type\\": \\"String\\", + \\"isRequired\\": true + } + } + } } }" `; @@ -2028,6 +2219,19 @@ exports[`Model Introspection Visitor Metadata snapshot should generate correct m } } } + }, + \\"inputs\\": { + \\"SimpleInput\\": { + \\"name\\": \\"SimpleInput\\", + \\"attributes\\": { + \\"name\\": { + \\"name\\": \\"name\\", + \\"isArray\\": false, + \\"type\\": \\"String\\", + \\"isRequired\\": false + } + } + } } }" `; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-visitor.test.ts.snap b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-visitor.test.ts.snap new file mode 100644 index 00000000..f7e1ca2a --- /dev/null +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-visitor.test.ts.snap @@ -0,0 +1,308 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppSyncModelVisitor Other GraphQL types shoud support query, mutation and subscription types 1`] = ` +Object { + "echo": Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "echo", + "operationType": "query", + "parameters": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "msg", + "type": "String", + }, + ], + "type": "String", + }, + "echo2": Object { + "baseType": "Todo", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "echo2", + "operationType": "query", + "parameters": Array [ + Object { + "baseType": "ID", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "todoId", + "type": "ID", + }, + ], + "type": "Todo", + }, + "echo3": Object { + "baseType": "Todo", + "directives": Array [], + "isList": true, + "isListNullable": false, + "isNullable": false, + "name": "echo3", + "operationType": "query", + "parameters": Array [], + "type": "Todo", + }, + "echo4": Object { + "baseType": "Phone", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "echo4", + "operationType": "query", + "parameters": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "number", + "type": "String", + }, + ], + "type": "Phone", + }, + "echo5": Object { + "baseType": "CustomUnion", + "directives": Array [], + "isList": true, + "isListNullable": false, + "isNullable": false, + "name": "echo5", + "operationType": "query", + "parameters": Array [], + "type": "CustomUnion", + }, + "echo6": Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "echo6", + "operationType": "query", + "parameters": Array [ + Object { + "baseType": "CustomInput", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "customInput", + "type": "CustomInput", + }, + ], + "type": "String", + }, + "echo7": Object { + "baseType": "ICustom", + "directives": Array [], + "isList": true, + "isListNullable": false, + "isNullable": true, + "name": "echo7", + "operationType": "query", + "parameters": Array [], + "type": "ICustom", + }, + "getAllTodo": Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "getAllTodo", + "operationType": "query", + "parameters": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "msg", + "type": "String", + }, + Object { + "baseType": "CustomInput", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "input", + "type": "CustomInput", + }, + ], + "type": "String", + }, +} +`; + +exports[`AppSyncModelVisitor Other GraphQL types shoud support query, mutation and subscription types 2`] = ` +Object { + "mutate": Object { + "baseType": "Todo", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "mutate", + "operationType": "mutation", + "parameters": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": true, + "isListNullable": false, + "isNullable": false, + "name": "msg", + "type": "String", + }, + ], + "type": "Todo", + }, +} +`; + +exports[`AppSyncModelVisitor Other GraphQL types shoud support query, mutation and subscription types 3`] = ` +Object { + "onMutate": Object { + "baseType": "Todo", + "directives": Array [], + "isList": true, + "isListNullable": true, + "isNullable": false, + "name": "onMutate", + "operationType": "subscription", + "parameters": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "msg", + "type": "String", + }, + ], + "type": "Todo", + }, +} +`; + +exports[`AppSyncModelVisitor Other GraphQL types should support input types 1`] = ` +Object { + "CustomInput": Object { + "inputValues": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "customField1", + "type": "String", + }, + Object { + "baseType": "BillingSource", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "customField2", + "type": "BillingSource", + }, + Object { + "baseType": "NestedInput", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "customField3", + "type": "NestedInput", + }, + ], + "name": "CustomInput", + "type": "input", + }, + "NestedInput": Object { + "inputValues": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "content", + "type": "String", + }, + ], + "name": "NestedInput", + "type": "input", + }, +} +`; + +exports[`AppSyncModelVisitor Other GraphQL types should support interface types 1`] = ` +Object { + "ICustom": Object { + "fields": Array [ + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "firstName", + "parameters": Array [], + "type": "String", + }, + Object { + "baseType": "String", + "directives": Array [], + "isList": false, + "isNullable": true, + "name": "lastName", + "parameters": Array [], + "type": "String", + }, + Object { + "baseType": "INestedCustom", + "directives": Array [], + "isList": true, + "isListNullable": false, + "isNullable": false, + "name": "birthdays", + "parameters": Array [], + "type": "INestedCustom", + }, + ], + "name": "ICustom", + "type": "interface", + }, + "INestedCustom": Object { + "fields": Array [ + Object { + "baseType": "AWSDate", + "directives": Array [], + "isList": false, + "isNullable": false, + "name": "birthDay", + "parameters": Array [], + "type": "AWSDate", + }, + ], + "name": "INestedCustom", + "type": "interface", + }, +} +`; + +exports[`AppSyncModelVisitor Other GraphQL types should support union types 1`] = ` +Object { + "CustomUnion": Object { + "name": "CustomUnion", + "type": "union", + "typeNames": Array [ + "Todo", + "Phone", + ], + }, +} +`; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-model-introspection-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-model-introspection-visitor.test.ts index bb2dab2a..98c2423d 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-model-introspection-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-model-introspection-visitor.test.ts @@ -53,6 +53,13 @@ describe('Model Introspection Visitor', () => { id: ID! names: [String] } + input SimpleInput { + name: String + } + interface SimpleInterface { + firstName: String! + } + union SimpleUnion = SimpleModel | SimpleEnum | SimpleNonModelType | SimpleInput | SimpleInterface `; const visitor: AppSyncModelIntrospectionVisitor = getVisitor(schema); describe('getType', () => { @@ -68,6 +75,18 @@ describe('Model Introspection Visitor', () => { expect((visitor as any).getType('SimpleNonModelType')).toEqual({ nonModel: 'SimpleNonModelType' }); }); + it('should return input type for Input', () => { + expect((visitor as any).getType('SimpleInput')).toEqual({ input: 'SimpleInput' }); + }); + + it('should return union type for Union', () => { + expect((visitor as any).getType('SimpleUnion')).toEqual({ union: 'SimpleUnion' }); + }); + + it('should return interface type for Interface', () => { + expect((visitor as any).getType('SimpleInterface')).toEqual({ interface: 'SimpleInterface' }); + }); + it('should throw error for unknown type', () => { expect(() => (visitor as any).getType('unknown')).toThrowError('Unknown type'); }); @@ -318,27 +337,63 @@ describe('schemas with pk on a belongsTo fk', () => { }); }); -describe('Custom queries/mutations/subscriptions tests', () => { +describe('Custom queries/mutations/subscriptions & input type tests', () => { const schema = /* GraphQL */ ` + input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY! + type Todo @model { id: ID! name: String! description: String + phone: Phone } type Phone { number: String } + enum BillingSource { + CLIENT + PROJECT + } + input CustomInput { + customField1: String! + customField2: Int + customField3: NestedInput! + } + input NestedInput { + content: String! = "hello" + } + interface ICustom { + firstName: String! + lastName: String + birthdays: [INestedCustom!]! + } + interface INestedCustom { + birthDay: AWSDate! + } + # The member types of a Union type must all be Object base types. + union CustomUnion = Todo | Phone + type Query { + getAllTodo(msg: String, input: CustomInput): String echo(msg: String): String echo2(todoId: ID!): Todo - echo3: [Todo] + echo3: [Todo!]! echo4(number: String): Phone + echo5: [CustomUnion!]! + echo6(customInput: CustomInput): String! + echo7: [ICustom]! + echo8(msg: [Float], msg2: [Int!], enumType: BillingSource, enumList: [BillingSource], inputType: [CustomInput]): [String] + echo9(msg: [Float]!, msg2: [Int!]!, enumType: BillingSource!, enumList: [BillingSource!]!, inputType: [CustomInput!]!): [String!]! } type Mutation { mutate(msg: [String!]!): Todo + mutate2: [CustomUnion!]! + mutate3: [ICustom]! } type Subscription { onMutate(msg: String): [Todo!] + onMutate2: CustomUnion + onMutate3: ICustom } `; it('should generate correct metadata for custom queries/mutations/subscriptions in model introspection schema', () => { diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-visitor.test.ts index 07300218..2f5fedc0 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-visitor.test.ts @@ -1339,4 +1339,75 @@ describe('AppSyncModelVisitor', () => { expect(projectTeamNameField.type).toBe('String'); }); }); + + describe('Other GraphQL types', () => { + const schema = /* GraphQL*/ ` + input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY! + + type Todo @model { + id: ID! + name: String! + description: String + phone: Phone + } + type Phone { + number: String + } + enum BillingSource { + CLIENT + PROJECT + } + input CustomInput { + customField1: String! + customField2: BillingSource + customField3: NestedInput! + } + input NestedInput { + content: String! = "hello" + } + interface ICustom { + firstName: String! + lastName: String + birthdays: [INestedCustom!]! + } + interface INestedCustom { + birthDay: AWSDate! + } + # The member types of a Union type must all be Object base types. + union CustomUnion = Todo | Phone + + type Query { + getAllTodo(msg: String, input: CustomInput): String + echo(msg: String): String + echo2(todoId: ID!): Todo + echo3: [Todo!]! + echo4(number: String): Phone + echo5: [CustomUnion!]! + echo6(customInput: CustomInput): String! + echo7: [ICustom]! + } + type Mutation { + mutate(msg: [String!]!): Todo + } + type Subscription { + onMutate(msg: String): [Todo!] + } + `; + const { queries, mutations, subscriptions, inputs, unions, interfaces } + = createAndGenerateVisitor(schema, { usePipelinedTransformer: true, respectPrimaryKeyAttributesOnConnectionField: true, transformerVersion: 2 }); + it('shoud support query, mutation and subscription types', () => { + expect(queries).toMatchSnapshot(); + expect(mutations).toMatchSnapshot(); + expect(subscriptions).toMatchSnapshot(); + }); + it('should support input types', () => { + expect(inputs).toMatchSnapshot(); + }); + it('should support union types', () => { + expect(unions).toMatchSnapshot(); + }); + it('should support interface types', () => { + expect(interfaces).toMatchSnapshot(); + }); + }) }); diff --git a/packages/appsync-modelgen-plugin/src/interfaces/introspection/model-schema.ts b/packages/appsync-modelgen-plugin/src/interfaces/introspection/model-schema.ts index aea3f33b..eba55bca 100644 --- a/packages/appsync-modelgen-plugin/src/interfaces/introspection/model-schema.ts +++ b/packages/appsync-modelgen-plugin/src/interfaces/introspection/model-schema.ts @@ -9,6 +9,7 @@ queries?: SchemaQueries; mutations?: SchemaMutations; subscriptions?: SchemaSubscriptions; + inputs?: SchemaInputs; }; /** * Top-level Entities on a Schema @@ -19,6 +20,7 @@ export type SchemaEnums = Record; export type SchemaQueries = Record; export type SchemaMutations = Record; export type SchemaSubscriptions = Record; +export type SchemaInputs = Record; export type SchemaModel = { name: string; @@ -56,7 +58,7 @@ export type Field = { association?: AssociationType; arguments?: Arguments; }; -export type FieldType = 'ID' +export type ScalarType = 'ID' | 'String' | 'Int' | 'Float' @@ -69,11 +71,22 @@ export type FieldType = 'ID' | 'AWSIPAddress' | 'Boolean' | 'AWSJSON' - | 'AWSPhone' + | 'AWSPhone'; +export type InputFieldType = ScalarType + | { enum: string } + | { input: string }; +export type FieldType = ScalarType | { enum: string } | { model: string } | { nonModel: string }; export type FieldAttribute = ModelAttribute; +/** + * Input Definition + */ +export type Input = { + name: string; + attributes: Arguments; +} /** * Field-level Relationship Definitions */ @@ -112,7 +125,7 @@ export type PrimaryKeyInfo = { export type Arguments = Record; export type Argument = { name: string; - type: FieldType; + type: InputFieldType; isArray: boolean; isRequired: boolean; isArrayNullable?: boolean; diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-model-introspection-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-model-introspection-visitor.ts index 9b1e645d..012597bc 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-model-introspection-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-model-introspection-visitor.ts @@ -1,13 +1,16 @@ import { DEFAULT_SCALARS, NormalizedScalarsMap } from "@graphql-codegen/visitor-plugin-common"; import { GraphQLSchema } from "graphql"; -import { Argument, AssociationType, Field, Fields, FieldType, ModelAttribute, ModelIntrospectionSchema, PrimaryKeyInfo, SchemaEnum, SchemaModel, SchemaMutation, SchemaNonModel, SchemaQuery, SchemaSubscription } from "../interfaces/introspection"; +import { Argument, AssociationType, Field, Fields, FieldType, ModelAttribute, ModelIntrospectionSchema, PrimaryKeyInfo, SchemaEnum, SchemaModel, SchemaMutation, SchemaNonModel, SchemaQuery, SchemaSubscription, Input, InputFieldType } from "../interfaces/introspection"; import { METADATA_SCALAR_MAP } from "../scalars"; import { CodeGenConnectionType } from "../utils/process-connections"; -import { RawAppSyncModelConfig, ParsedAppSyncModelConfig, AppSyncModelVisitor, CodeGenEnum, CodeGenField, CodeGenModel, CodeGenPrimaryKeyType, CodeGenQuery, CodeGenSubscription, CodeGenMutation } from "./appsync-visitor"; +import { RawAppSyncModelConfig, ParsedAppSyncModelConfig, AppSyncModelVisitor, CodeGenEnum, CodeGenField, CodeGenModel, CodeGenPrimaryKeyType, CodeGenQuery, CodeGenSubscription, CodeGenMutation, CodeGenInputObject, CodeGenUnion, CodeGenInterface } from "./appsync-visitor"; import fs from 'fs'; import path from 'path'; import Ajv from 'ajv'; +type UnionFieldType = { union: string }; +type InterfaceFieldType = { interface: string }; + export interface RawAppSyncModelIntrospectionConfig extends RawAppSyncModelConfig {}; export interface ParsedAppSyncModelIntrospectionConfig extends ParsedAppSyncModelConfig {}; export class AppSyncModelIntrospectionVisitor< @@ -65,23 +68,47 @@ export class AppSyncModelIntrospectionVisitor< }, {}); result = { ...result, models, nonModels, enums }; const queries = Object.values(this.queryMap).reduce((acc, queryObj: CodeGenQuery) => { + // Skip the field if the field type is union/interface + // TODO: Remove this skip once these types are supported for stakeholder usages + const fieldType = this.getType(queryObj.type) as any; + if (this.isUnionFieldType(fieldType) || this.isInterfaceFieldType(fieldType)) { + return acc; + } return { ...acc, [queryObj.name]: this.generateGraphQLOperationMetadata(queryObj) }; }, {}) const mutations = Object.values(this.mutationMap).reduce((acc, mutationObj: CodeGenMutation) => { + // Skip the field if the field type is union/interface + // TODO: Remove this skip once these types are supported for stakeholder usages + const fieldType = this.getType(mutationObj.type) as any; + if (this.isUnionFieldType(fieldType) || this.isInterfaceFieldType(fieldType)) { + return acc; + } return { ...acc, [mutationObj.name]: this.generateGraphQLOperationMetadata(mutationObj) }; }, {}); const subscriptions = Object.values(this.subscriptionMap).reduce((acc, subscriptionObj: CodeGenSubscription) => { + // Skip the field if the field type is union/interface + // TODO: Remove this skip once these types are supported for stakeholder usages + const fieldType = this.getType(subscriptionObj.type) as any; + if (this.isUnionFieldType(fieldType) || this.isInterfaceFieldType(fieldType)) { + return acc; + } return { ...acc, [subscriptionObj.name]: this.generateGraphQLOperationMetadata(subscriptionObj) }; - }, {}) - if(Object.keys(queries).length > 0) { + }, {}); + const inputs = Object.values(this.inputObjectMap).reduce((acc, inputObj: CodeGenInputObject) => { + return { ...acc, [inputObj.name]: this.generateGraphQLInputMetadata(inputObj) }; + }, {}); + if (Object.keys(queries).length > 0) { result = { ...result, queries }; } - if(Object.keys(mutations).length > 0) { + if (Object.keys(mutations).length > 0) { result = { ...result, mutations }; } - if(Object.keys(subscriptions).length > 0) { + if (Object.keys(subscriptions).length > 0) { result = { ...result, subscriptions }; } + if (Object.keys(inputs).length > 0) { + result = { ...result, inputs } + } return result; } @@ -121,10 +148,16 @@ export class AppSyncModelIntrospectionVisitor< return { name: this.getModelName(nonModel), fields: nonModel.fields.reduce((acc: Fields, field: CodeGenField) => { + // Skip the field if the field type is union/interface + // TODO: Remove this skip once these types are supported for stakeholder usages + const fieldType = this.getType(field.type) as any; + if (this.isUnionFieldType(fieldType) || this.isInterfaceFieldType(fieldType)) { + return acc; + } const fieldMeta: Field = { name: this.getFieldName(field), isArray: field.isList, - type: this.getType(field.type), + type: fieldType, isRequired: !field.isNullable, attributes: [], }; @@ -152,6 +185,30 @@ export class AppSyncModelIntrospectionVisitor< values: Object.values(enumObj.values), }; } + + /** + * Generate GraqhQL input object type metadata in model introspection schema from the codegen MIPR + * @param inputObj input type object + * @returns input type object metadata in model introspection schema + */ + private generateGraphQLInputMetadata(inputObj: CodeGenInputObject): Input { + return { + name: inputObj.name, + attributes: inputObj.inputValues.reduce((acc, param ) => { + const arg: Argument = { + name: param.name, + isArray: param.isList, + type: this.getType(param.type) as InputFieldType, + isRequired: !param.isNullable + }; + if (param.isListNullable !== undefined) { + arg.isArrayNullable = param.isListNullable; + } + return { ...acc, [param.name]: arg }; + }, {}), + } + } + /** * Generate GraqhQL operation (query/mutation/subscription) metadata in model introspection schema from the codegen MIPR * @param operationObj operation object @@ -172,7 +229,7 @@ export class AppSyncModelIntrospectionVisitor< const arg: Argument = { name: param.name, isArray: param.isList, - type: this.getType(param.type), + type: this.getType(param.type) as InputFieldType, isRequired: !param.isNullable }; if (param.isListNullable !== undefined) { @@ -184,7 +241,7 @@ export class AppSyncModelIntrospectionVisitor< return operationMeta as V; } - protected getType(gqlType: string): FieldType { + protected getType(gqlType: string): FieldType | InputFieldType | UnionFieldType | InterfaceFieldType { // Todo: Handle unlisted scalars if (gqlType in METADATA_SCALAR_MAP) { return METADATA_SCALAR_MAP[gqlType] as FieldType; @@ -198,7 +255,16 @@ export class AppSyncModelIntrospectionVisitor< if (gqlType in this.modelMap) { return { model: gqlType }; } - throw new Error(`Unknown type ${gqlType}`); + if (gqlType in this.inputObjectMap) { + return { input: gqlType } + } + if (gqlType in this.unionMap) { + return { union: gqlType } + } + if (gqlType in this.interfaceMap) { + return { interface: gqlType } + } + throw new Error(`Unknown type ${gqlType} found during model introspection schema generation`); } private generateModelPrimaryKeyInfo(model: CodeGenModel): PrimaryKeyInfo { @@ -213,4 +279,11 @@ export class AppSyncModelIntrospectionVisitor< } throw new Error(`No primary key found for model ${model.name}`); } + + private isUnionFieldType = (obj: any): obj is UnionFieldType => { + return typeof obj === 'object' && typeof obj.union === 'string'; + } + private isInterfaceFieldType = (obj: any): obj is InterfaceFieldType => { + return typeof obj === 'object' && typeof obj.interface === 'string'; + } } \ No newline at end of file diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts index 34c2de3f..603ff91c 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts @@ -21,6 +21,9 @@ import { parse, valueFromASTUntyped, InputValueDefinitionNode, + InputObjectTypeDefinitionNode, + UnionTypeDefinitionNode, + InterfaceTypeDefinitionNode, } from 'graphql'; import { addFieldToModel, getModelPrimaryKeyComponentFields, removeFieldFromModel, toCamelCase } from '../utils/fieldUtils'; import { getTypeInfo } from '../utils/get-type-info'; @@ -250,8 +253,29 @@ export type CodeGenMutationMap = Record; export type CodeGenSubscription = CodeGenField & { operationType: 'subscription'; }; +export type CodeGenInputObject = { + name: string; + type: 'input'; + inputValues: CodeGenInputValues +} export type CodeGenSubscriptionMap = Record; +export type CodeGenInputObjectMap = Record + +export type CodeGenUnion = { + name: string; + type: 'union'; + typeNames: string[] +}; +export type CodeGenUnionMap = Record; + +export type CodeGenInterface = { + name: string; + type: 'interface'; + fields: CodeGenField[]; +}; +export type CodeGenInterfaceMap = Record; + // Used to simplify processing of manyToMany into composing directives hasMany and belongsTo type ManyToManyContext = { model: CodeGenModel; @@ -271,7 +295,10 @@ export class AppSyncModelVisitor< protected queryMap: CodeGenQueryMap = {}; protected mutationMap: CodeGenMutationMap = {}; protected subscriptionMap: CodeGenSubscriptionMap = {}; - protected typesToSkip: string[] = []; + protected inputObjectMap: CodeGenInputObjectMap = {}; + protected unionMap: CodeGenUnionMap = {}; + protected interfaceMap: CodeGenInterfaceMap = {}; + protected typesToSkip: string[] = ['AMPLIFY']; constructor( protected _schema: GraphQLSchema, rawConfig: TRawConfig, @@ -303,7 +330,6 @@ export class AppSyncModelVisitor< }); } - this.typesToSkip = []; this.typesToSkip.push(...typesUsedInDirectives); } @@ -380,6 +406,19 @@ export class AppSyncModelVisitor< }; } + InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode) { + if (this.typesToSkip.includes(node.name.value)) { + return; + } + const inputValues = (node.fields as unknown) as CodeGenInputValue[]; + const inputObject: CodeGenInputObject = { + name: node.name.value, + type: 'input', + inputValues, + }; + this.inputObjectMap[node.name.value] = inputObject; + } + InputValueDefinition(node: InputValueDefinitionNode): CodeGenInputValue { const directives = this.getDirectives(node.directives); return { @@ -407,6 +446,32 @@ export class AppSyncModelVisitor< values, }; } + + UnionTypeDefinition(node: UnionTypeDefinitionNode): void { + if (this.typesToSkip.includes(node.name.value)) { + return; + } + const unionObject: CodeGenUnion = { + name: node.name.value, + type: 'union', + typeNames: node.types?.map(type => type.name.value) ?? [], + } + this.unionMap[node.name.value] = unionObject; + } + + InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode): void { + if (this.typesToSkip.includes(node.name.value)) { + return; + } + const fields = (node.fields as unknown) as CodeGenField[]; + const interfaceEntry: CodeGenInterface = { + name: node.name.value, + type: 'interface', + fields, + }; + this.interfaceMap[node.name.value] = interfaceEntry; + } + processDirectives( // TODO: Remove us when we have a fix to roll-forward. shouldUseModelNameFieldInHasManyAndBelongsTo: boolean, @@ -1232,4 +1297,22 @@ export class AppSyncModelVisitor< get nonModels() { return this.nonModelMap; } + get queries() { + return this.queryMap; + } + get mutations() { + return this.mutationMap; + } + get subscriptions() { + return this.subscriptionMap; + } + get inputs() { + return this.inputObjectMap; + } + get unions() { + return this.unionMap; + } + get interfaces() { + return this.interfaceMap; + } }