diff --git a/.codebuild/e2e_workflow.yml b/.codebuild/e2e_workflow.yml index f0e8c945b..3e2b9f315 100644 --- a/.codebuild/e2e_workflow.yml +++ b/.codebuild/e2e_workflow.yml @@ -145,25 +145,25 @@ batch: depend-on: - publish_to_local_registry - identifier: >- - l_build_app_ts_graphql_generator_app_uninitialized_project_codegen_js_uninitialized_project_modelgen_android + l_build_app_ts_graphql_generator_app_push_codegen_admin_modelgen_uninitialized_project_codegen_js buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: TEST_SUITE: >- - src/__tests__/build-app-ts.test.ts|src/__tests__/graphql-generator-app.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts + src/__tests__/build-app-ts.test.ts|src/__tests__/graphql-generator-app.test.ts|src/__tests__/push-codegen-admin-modelgen.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts CLI_REGION: ap-southeast-1 DISABLE_ESLINT_PLUGIN: true depend-on: - publish_to_local_registry - identifier: >- - l_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js + l_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios_uninitialize buildspec: .codebuild/run_e2e_tests.yml env: compute-type: BUILD_GENERAL1_LARGE variables: TEST_SUITE: >- - src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts + src/__tests__/uninitialized-project-modelgen-android.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 @@ -252,7 +252,7 @@ batch: - publish_to_local_registry - build_windows - identifier: >- - w_build_app_ts_graphql_generator_app_uninitialized_project_codegen_js_uninitialized_project_modelgen_android + w_build_app_ts_graphql_generator_app_push_codegen_admin_modelgen_uninitialized_project_codegen_js buildspec: .codebuild/run_e2e_tests_windows.yml env: compute-type: BUILD_GENERAL1_LARGE @@ -260,14 +260,14 @@ batch: type: WINDOWS_SERVER_2019_CONTAINER variables: TEST_SUITE: >- - src/__tests__/build-app-ts.test.ts|src/__tests__/graphql-generator-app.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts|src/__tests__/uninitialized-project-modelgen-android.test.ts + src/__tests__/build-app-ts.test.ts|src/__tests__/graphql-generator-app.test.ts|src/__tests__/push-codegen-admin-modelgen.test.ts|src/__tests__/uninitialized-project-codegen-js.test.ts CLI_REGION: us-east-1 DISABLE_ESLINT_PLUGIN: true depend-on: - publish_to_local_registry - build_windows - identifier: >- - w_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios_uninitialized_project_modelgen_js + w_uninitialized_project_modelgen_android_uninitialized_project_modelgen_flutter_uninitialized_project_modelgen_ios_uninitialize buildspec: .codebuild/run_e2e_tests_windows.yml env: compute-type: BUILD_GENERAL1_LARGE @@ -275,7 +275,7 @@ batch: type: WINDOWS_SERVER_2019_CONTAINER variables: TEST_SUITE: >- - src/__tests__/uninitialized-project-modelgen-flutter.test.ts|src/__tests__/uninitialized-project-modelgen-ios.test.ts|src/__tests__/uninitialized-project-modelgen-js.test.ts + src/__tests__/uninitialized-project-modelgen-android.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/.github/workflows/closed-issue-message.yml b/.github/workflows/closed-issue-message.yml new file mode 100644 index 000000000..8cb2db494 --- /dev/null +++ b/.github/workflows/closed-issue-message.yml @@ -0,0 +1,15 @@ +name: Closed Issue Message +on: + issues: + types: [closed] +jobs: + auto_comment: + runs-on: ubuntu-latest + steps: + - uses: aws-actions/closed-issue-message@v1 + with: + # These inputs are both required + repo-token: '${{ secrets.GITHUB_TOKEN }}' + message: | + This issue is now closed. Comments on closed issues are hard for our team to see. + If you need more assistance, please open a new issue that references this one. diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c13259cb..dce1d156a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,7 @@ "eslint.packageManager": "yarn", "eslint.quiet": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "jest.enableInlineErrorMessages": true, "jest.showCoverageOnLoad": true, diff --git a/README-relationships.md b/README-relationships.md new file mode 100644 index 000000000..100e1acbe --- /dev/null +++ b/README-relationships.md @@ -0,0 +1,262 @@ +# Modeling relationships in the introspection schema + +## Background + +The Model Introspection Schema (MIS) is an intermediate representation of the GraphQL model that includes Amplify annotations. It is different +from the standard [GraphQL introspection schema](https://graphql.org/learn/introspection/) in that it includes relationship information, not +just type information. + +> **NOTE:** The MIS is an internal implementation detail of the Amplify API plugin. It should not be used in a customer application. + +## Sample + +Given a schema like + +```graphql +type Primary @model @auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) { + id: ID! @primaryKey + relatedMany: [RelatedMany] @hasMany(references: "primaryId") + relatedOne: RelatedOne @hasOne(references: "primaryId") +} + +type RelatedMany @model @auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) { + id: ID! @primaryKey + primaryId: ID! + primary: Primary @belongsTo(references: "primaryId") +} + +type RelatedOne @model @auth(rules: [{ allow: public, operations: [read] }, { allow: owner }]) { + id: ID! @primaryKey + primaryId: ID! + primary: Primary @belongsTo(references: "primaryId") +} +``` + +the MIS (abridged to show relationship information only) looks like: + +```json +{ + "version": 1, + "models": { + "Primary": { + "name": "Primary", + "fields": { + "relatedMany": { + "name": "relatedMany", + "isArray": true, + "type": { + "model": "RelatedMany" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": ["primaryId"] + } + }, + "relatedOne": { + "name": "relatedOne", + "isArray": false, + "type": { + "model": "RelatedOne" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "HAS_ONE", + "associatedWith": ["primaryId"], + } + } + } + }, + "RelatedMany": { + "name": "RelatedMany", + "fields": { + "primary": { + "name": "primary", + "isArray": false, + "type": { + "model": "Primary" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetNames": ["primaryId"] + } + }, + "primaryId": { + "name": "primaryId", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + } + } + }, + "RelatedOne": { + "name": "RelatedOne", + "fields": { + "primary": { + "name": "primary", + "isArray": false, + "type": { + "model": "Primary" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetNames": ["primaryId"] + } + }, + "primaryId": { + "name": "primaryId", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + } + } + } + } +} +``` + +## Glossary + +* **Associated type** - In a field decorated with a `@hasMany`, `@hasOne`, or `@belongsTo` directive, the model “pointed to” by the directive. In the sample schema: + * `Related` is the **associated type** for the `@hasMany` directive on `Primary.related` + * `Primary` is the **associated type** for the `@belongsTo` directive on `Related.primary` +* **Association field** - See **Connection field** +* **Connection field** - In any model type, the field that is decorated with a `@hasMany`, `@hasOne`, or `@belongsTo` directive. In the sample schema: + * `Primary.related` is the **connection field** in the `Primary` model, for the relationship `Primary -> Related` defined by the `@hasMany` on `Primary.related` and the `@belongsTo` on `Related.primary` + * `Related.primary` is the **connection field** in the `Related` model, for the relationship `Primary -> Related` defined by the `@hasMany` on `Primary.related` and the `@belongsTo` on `Related.primary` +* **Source type** - In a field decorated with a `@hasMany`, `@hasOne`, or `@belongsTo` directive, the model containing the directive. In the sample schema: + * `Primary` is the **source type** for the `@hasMany` directive on `Primary.related` + * `Related` is the **source type** for the `@belongsTo` directive on `Related.primary` + +## Structure + +Relationships are modeled in an `association` structure in the MIS. The `association` attribute must belong to a `@model` field, not a field of non-model type, enum, input, or custom query/mutation. + +Here are the relevant types to define the association structure. Note that this is a simplified rendition of the JSON/JavaScript version of the MIS. Other platforms may represent the MIS differently. The full definition is in [source code](./appsync-modelgen-plugin/src/utils/process-connections.ts); + +```ts +enum CodeGenConnectionType { + HAS_ONE = 'HAS_ONE', + BELONGS_TO = 'BELONGS_TO', + HAS_MANY = 'HAS_MANY', +} + +type CodeGenConnectionTypeBase = { + kind: CodeGenConnectionType; + connectedModel: CodeGenModel; + // ^-- Type not shown +}; + +type CodeGenFieldConnectionBelongsTo = CodeGenConnectionTypeBase & { + kind: CodeGenConnectionType.BELONGS_TO; + targetNames: string[]; +} + +type CodeGenFieldConnectionHasOne = CodeGenConnectionTypeBase & { + kind: CodeGenConnectionType.HAS_ONE; + associatedWith: CodeGenField[]; + // ^-- Type not shown -- rendered in MIS as a string array + targetNames: string[]; +} + +export type CodeGenFieldConnectionHasMany = CodeGenConnectionTypeBase & { + kind: CodeGenConnectionType.HAS_MANY; + associatedWith: CodeGenField[]; + // ^-- Type not shown -- rendered in MIS as a string array +} +``` + +Considering a snippet of the above sample: + +```json + "models": { + "Primary": { + "name": "Primary", + "fields": { + "relatedMany": { + "name": "relatedMany", + "isArray": true, + "type": { + "model": "RelatedMany" + }, + "isRequired": false, + "attributes": [], + "isArrayNullable": true, + "association": { + "connectionType": "HAS_MANY", + "associatedWith": ["primaryId"] + } + }, +... + "RelatedMany": { + "name": "RelatedMany", + "fields": { + "primary": { + "name": "primary", + "isArray": false, + "type": { + "model": "Primary" + }, + "isRequired": false, + "attributes": [], + "association": { + "connectionType": "BELONGS_TO", + "targetNames": ["primaryId"] + } + }, + "primaryId": { + "name": "primaryId", + "isArray": false, + "type": "ID", + "isRequired": false, + "attributes": [] + } +``` + +- `models.Primary` - A type definition. The **source type** for any `association`s defined in this model. +- `models.Primary.fields.relatedMany` - The **association field**/**connection field** +- `models.Primary.fields.relatedMany.type` - The **associated type** for this relationship. This must be a `@model`. +- `models.Primary.fields.relatedMany.association` - The structure containing the data needed to navigate the relationship with the associated type +- `models.Primary.fields.relatedMany.association.connectionType` - The kind of relationship (has one, has many, belongs to) this **source type** has with the associated type +- `models.Primary.fields.relatedMany.association.associatedWith` - A list of fields on the **associated type** that hold the primary key of the **source** record. This is an array so we can support composite primary keys. +- `models.RelatedMany` - A type definition. The **source type** for any `association`s defined in this model. +- `models.RelatedMany.fields.primary.association.targetNames` - A list of fields on the **source type** (that is, the current type) that hold the primary key of the **associated** record. This is an array so we can support composite primary keys. +- `models.RelatedMany.fields.primaryId` - The field pointed to by `targetNames` above, containing the primary key of the **associated** record for the `RelatedOne.primary` relationship. + + +## Navigating relationships + +We will describe the steps to resolve the record in pseudo-sql + +### From source record to associated record + +* If the source model has an `associatedWith` but no `targetNames`: + ``` + SELECT * + FROM + WHERE = .primaryKey + ``` +* If the source model has an `associatedWith` AND `targetNames`: + ``` + SELECT * + FROM + WHERE = . + ``` +* If the source model has a `targetNames` but no `associatedWith`: + ``` + SELECT * + FROM + WHERE . = .primaryKey + ``` + + + diff --git a/dependency_licenses.txt b/dependency_licenses.txt index 90e069522..54177154c 100644 --- a/dependency_licenses.txt +++ b/dependency_licenses.txt @@ -684,7 +684,7 @@ Apache License ----- -The following software may be included in this product: @aws-amplify/graphql-schema-test-library, @aws-amplify/graphql-transformer-interfaces, graphql-mapping-template, graphql-transformer-common, graphql-transformer-core. A copy of the source code may be downloaded from https://github.com/aws-amplify/amplify-category-api.git (@aws-amplify/graphql-schema-test-library), https://github.com/aws-amplify/amplify-category-api.git (@aws-amplify/graphql-transformer-interfaces), https://github.com/aws-amplify/amplify-category-api.git (graphql-mapping-template), https://github.com/aws-amplify/amplify-category-api.git (graphql-transformer-common), https://github.com/aws-amplify/amplify-category-api.git (graphql-transformer-core). This software contains the following license and notice below: +The following software may be included in this product: @aws-amplify/graphql-directives, @aws-amplify/graphql-schema-test-library, @aws-amplify/graphql-transformer-interfaces, graphql-mapping-template, graphql-transformer-common, graphql-transformer-core. A copy of the source code may be downloaded from https://github.com/aws-amplify/amplify-category-api.git (@aws-amplify/graphql-directives), https://github.com/aws-amplify/amplify-category-api.git (@aws-amplify/graphql-schema-test-library), https://github.com/aws-amplify/amplify-category-api.git (@aws-amplify/graphql-transformer-interfaces), https://github.com/aws-amplify/amplify-category-api.git (graphql-mapping-template), https://github.com/aws-amplify/amplify-category-api.git (graphql-transformer-common), https://github.com/aws-amplify/amplify-category-api.git (graphql-transformer-core). This software contains the following license and notice below: Apache License Version 2.0, January 2004 diff --git a/packages/amplify-codegen-e2e-core/CHANGELOG.md b/packages/amplify-codegen-e2e-core/CHANGELOG.md index 58a342919..0171d37f8 100644 --- a/packages/amplify-codegen-e2e-core/CHANGELOG.md +++ b/packages/amplify-codegen-e2e-core/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.6.4](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-core@1.6.3...@aws-amplify/amplify-codegen-e2e-core@1.6.4) (2024-04-03) + +### Bug Fixes + +- process input object, union and interface metadata in model introspection schema codegen ([#795](https://github.com/aws-amplify/amplify-codegen/issues/795)) ([73e4520](https://github.com/aws-amplify/amplify-codegen/commit/73e4520e8f3bbd63d6b123a5c977c415df443905)) + ## [1.6.3](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-core@1.6.2...@aws-amplify/amplify-codegen-e2e-core@1.6.3) (2023-12-11) **Note:** Version bump only for package @aws-amplify/amplify-codegen-e2e-core diff --git a/packages/amplify-codegen-e2e-core/package.json b/packages/amplify-codegen-e2e-core/package.json index b8a1327f7..ecdcf4710 100644 --- a/packages/amplify-codegen-e2e-core/package.json +++ b/packages/amplify-codegen-e2e-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/amplify-codegen-e2e-core", - "version": "1.6.3", + "version": "1.6.4", "description": "", "repository": { "type": "git", 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 4987ec612..f132944b0 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/CHANGELOG.md b/packages/amplify-codegen-e2e-tests/CHANGELOG.md index 449b8b892..db948c6e8 100644 --- a/packages/amplify-codegen-e2e-tests/CHANGELOG.md +++ b/packages/amplify-codegen-e2e-tests/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.44.4](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-tests@2.44.3...@aws-amplify/amplify-codegen-e2e-tests@2.44.4) (2024-04-03) + +### Bug Fixes + +- process input object, union and interface metadata in model introspection schema codegen ([#795](https://github.com/aws-amplify/amplify-codegen/issues/795)) ([73e4520](https://github.com/aws-amplify/amplify-codegen/commit/73e4520e8f3bbd63d6b123a5c977c415df443905)) + ## [2.44.3](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/amplify-codegen-e2e-tests@2.44.2...@aws-amplify/amplify-codegen-e2e-tests@2.44.3) (2023-12-11) **Note:** Version bump only for package @aws-amplify/amplify-codegen-e2e-tests diff --git a/packages/amplify-codegen-e2e-tests/package.json b/packages/amplify-codegen-e2e-tests/package.json index 250d8bb69..15f89248f 100644 --- a/packages/amplify-codegen-e2e-tests/package.json +++ b/packages/amplify-codegen-e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/amplify-codegen-e2e-tests", - "version": "2.44.3", + "version": "2.44.4", "description": "", "repository": { "type": "git", @@ -22,7 +22,7 @@ "clean-e2e-resources": "ts-node ./src/cleanup-e2e-resources.ts" }, "dependencies": { - "@aws-amplify/amplify-codegen-e2e-core": "1.6.3", + "@aws-amplify/amplify-codegen-e2e-core": "1.6.4", "@aws-amplify/graphql-schema-test-library": "^1.1.18", "amazon-cognito-identity-js": "^6.3.6", "aws-amplify": "^5.3.3", 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 000000000..a2ecffa17 --- /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 000000000..5c7bedb1d --- /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 51d6de17e..a09976c5f 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 d53158337..20e65dc7d 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/amplify-codegen/CHANGELOG.md b/packages/amplify-codegen/CHANGELOG.md index 29cc83187..33fa17a92 100644 --- a/packages/amplify-codegen/CHANGELOG.md +++ b/packages/amplify-codegen/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.8.0](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.7.4...amplify-codegen@4.8.0) (2024-04-03) + +### Features + +- add angular codegen v6 support ([#799](https://github.com/aws-amplify/amplify-codegen/issues/799)) ([7d1a269](https://github.com/aws-amplify/amplify-codegen/commit/7d1a26941547a26640f7dc4aa25da9c0e1dab654)) +- use default directives from @aws-amplify/graphql-directives ([#796](https://github.com/aws-amplify/amplify-codegen/issues/796)) ([a94649e](https://github.com/aws-amplify/amplify-codegen/commit/a94649ef5cbed1091e4c206852d85f4b860a3eae)) + ## [4.7.4](https://github.com/aws-amplify/amplify-codegen/compare/amplify-codegen@4.7.3...amplify-codegen@4.7.4) (2024-01-29) **Note:** Version bump only for package amplify-codegen diff --git a/packages/amplify-codegen/package.json b/packages/amplify-codegen/package.json index 7ffe230d8..c9ab262d9 100644 --- a/packages/amplify-codegen/package.json +++ b/packages/amplify-codegen/package.json @@ -1,6 +1,6 @@ { "name": "amplify-codegen", - "version": "4.7.4", + "version": "4.8.0", "description": "Amplify Code Generator", "repository": { "type": "git", @@ -21,9 +21,10 @@ "extract-api": "ts-node ../../scripts/extract-api.ts" }, "dependencies": { + "@aws-amplify/graphql-directives": "^1.0.1", "@aws-amplify/graphql-docs-generator": "4.2.1", - "@aws-amplify/graphql-generator": "0.2.4", - "@aws-amplify/graphql-types-generator": "3.4.6", + "@aws-amplify/graphql-generator": "0.3.0", + "@aws-amplify/graphql-types-generator": "3.5.0", "@graphql-codegen/core": "2.6.6", "chalk": "^3.0.0", "fs-extra": "^8.1.0", diff --git a/packages/amplify-codegen/src/commands/add.js b/packages/amplify-codegen/src/commands/add.js index fc396fc7a..a702bd3fe 100644 --- a/packages/amplify-codegen/src/commands/add.js +++ b/packages/amplify-codegen/src/commands/add.js @@ -146,6 +146,8 @@ async function add(context, apiId = null, region = 'us-east-1') { apiId, ...(withoutInit ? { frontend } : {}), ...(withoutInit && frontend === 'javascript' ? { framework } : {}), + // The default Amplify JS lib version is set for 6 for angular codegen + ...(answer.target === 'angular' ? { amplifyJsLibraryVersion: 6 } : {}), }, }; diff --git a/packages/amplify-codegen/src/commands/models.js b/packages/amplify-codegen/src/commands/models.js index 66dd2da3d..fffd26f24 100644 --- a/packages/amplify-codegen/src/commands/models.js +++ b/packages/amplify-codegen/src/commands/models.js @@ -3,8 +3,8 @@ const fs = require('fs-extra'); const globby = require('globby'); const { FeatureFlags, pathManager } = require('@aws-amplify/amplify-cli-core'); const { generateModels: generateModelsHelper } = require('@aws-amplify/graphql-generator'); +const { DefaultDirectives } = require('@aws-amplify/graphql-directives'); const { validateAmplifyFlutterMinSupportedVersion } = require('../utils/validateAmplifyFlutterMinSupportedVersion'); -const defaultDirectiveDefinitions = require('../utils/defaultDirectiveDefinitions'); const getProjectRoot = require('../utils/getProjectRoot'); const { getModelSchemaPathParam, hasModelSchemaPathParam } = require('../utils/getModelSchemaPathParam'); const { isDataStoreEnabled } = require('graphql-transformer-core'); @@ -131,7 +131,7 @@ const getDirectives = async (context, apiResourcePath) => { resourceDir: apiResourcePath, }); } catch { - return defaultDirectiveDefinitions; + return DefaultDirectives.map(directive => directive.definition).join('\n'); } }; diff --git a/packages/amplify-codegen/src/commands/types.js b/packages/amplify-codegen/src/commands/types.js index a42763a6f..2dc4f4c60 100644 --- a/packages/amplify-codegen/src/commands/types.js +++ b/packages/amplify-codegen/src/commands/types.js @@ -58,6 +58,21 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, } const target = cfg.amplifyExtension.codeGenTarget; + let amplifyJsLibraryVersion = cfg.amplifyExtension.amplifyJsLibraryVersion; + + /** + * amplifyJsLibraryVersion config is currently used for angular codegen + * The supported value is 5 for JS v5 and 6 for JS v6 + * When the value is undefined, it will stay at codegen for JS v5 for non-breaking change for existing users + * For the other values set, a warning message will be sent in the console and the codegen will use the v6 codegen + */ + if (target === 'angular' && amplifyJsLibraryVersion && amplifyJsLibraryVersion !== 6 && amplifyJsLibraryVersion !== 5) { + context.print.warning( + `Amplify JS library version ${amplifyJsLibraryVersion} is not supported. The current support JS library version is [5, 6]. Codegen will be executed for JS v6 instead.`, + ); + amplifyJsLibraryVersion = 6 + } + const excludes = cfg.excludes.map(pattern => `!${pattern}`); const normalizedPatterns = [...includeFiles, ...excludes].map((path) => normalizePathForGlobPattern(path)); const queryFilePaths = globby.sync(normalizedPatterns, { @@ -114,6 +129,7 @@ async function generateTypes(context, forceDownloadSchema, withoutInit = false, target, introspection, multipleSwiftFiles, + amplifyJsLibraryVersion, }); const outputs = Object.entries(output); diff --git a/packages/amplify-codegen/src/utils/defaultDirectiveDefinitions.js b/packages/amplify-codegen/src/utils/defaultDirectiveDefinitions.js deleted file mode 100644 index f0254f38f..000000000 --- a/packages/amplify-codegen/src/utils/defaultDirectiveDefinitions.js +++ /dev/null @@ -1,113 +0,0 @@ -const defaultDirectiveDefinitions = ` -directive @aws_subscribe(mutations: [String!]!) on FIELD_DEFINITION - -directive @aws_auth(cognito_groups: [String!]!) on FIELD_DEFINITION - -directive @aws_api_key on FIELD_DEFINITION | OBJECT - -directive @aws_iam on FIELD_DEFINITION | OBJECT - -directive @aws_oidc on FIELD_DEFINITION | OBJECT - -directive @aws_cognito_user_pools(cognito_groups: [String!]) on FIELD_DEFINITION | OBJECT - -directive @aws_lambda on FIELD_DEFINITION | OBJECT - -directive @deprecated(reason: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ENUM | ENUM_VALUE - -directive @model(queries: ModelQueryMap, mutations: ModelMutationMap, subscriptions: ModelSubscriptionMap, timestamps: TimestampConfiguration) on OBJECT -input ModelMutationMap { - create: String - update: String - delete: String -} -input ModelQueryMap { - get: String - list: String -} -input ModelSubscriptionMap { - onCreate: [String] - onUpdate: [String] - onDelete: [String] - level: ModelSubscriptionLevel -} -enum ModelSubscriptionLevel { - off - public - on -} -input TimestampConfiguration { - createdAt: String - updatedAt: String -} -directive @function(name: String!, region: String, accountId: String) repeatable on FIELD_DEFINITION -directive @http(method: HttpMethod = GET, url: String!, headers: [HttpHeader] = []) on FIELD_DEFINITION -enum HttpMethod { - GET - POST - PUT - DELETE - PATCH -} -input HttpHeader { - key: String - value: String -} -directive @predictions(actions: [PredictionsActions!]!) on FIELD_DEFINITION -enum PredictionsActions { - identifyText - identifyLabels - convertTextToSpeech - translateText -} -directive @primaryKey(sortKeyFields: [String]) on FIELD_DEFINITION -directive @index(name: String, sortKeyFields: [String], queryField: String) repeatable on FIELD_DEFINITION -directive @hasMany(indexName: String, fields: [String!], limit: Int = 100) on FIELD_DEFINITION -directive @hasOne(fields: [String!]) on FIELD_DEFINITION -directive @manyToMany(relationName: String!, limit: Int = 100) on FIELD_DEFINITION -directive @belongsTo(fields: [String!]) on FIELD_DEFINITION -directive @default(value: String!) on FIELD_DEFINITION -directive @auth(rules: [AuthRule!]!) on OBJECT | FIELD_DEFINITION -input AuthRule { - allow: AuthStrategy! - provider: AuthProvider - identityClaim: String - groupClaim: String - ownerField: String - groupsField: String - groups: [String] - operations: [ModelOperation] -} -enum AuthStrategy { - owner - groups - private - public - custom -} -enum AuthProvider { - apiKey - iam - oidc - userPools - function -} -enum ModelOperation { - create - update - delete - read - list - get - sync - listen - search -} -directive @mapsTo(name: String!) on OBJECT -directive @searchable(queries: SearchableQueryMap) on OBJECT -input SearchableQueryMap { - search: String -} -`; - -module.exports = defaultDirectiveDefinitions; diff --git a/packages/amplify-codegen/src/utils/index.js b/packages/amplify-codegen/src/utils/index.js index 25d2a1e89..3001062ea 100644 --- a/packages/amplify-codegen/src/utils/index.js +++ b/packages/amplify-codegen/src/utils/index.js @@ -16,7 +16,6 @@ const getSDLSchemaLocation = require('./getSDLSchemaLocation'); const switchToSDLSchema = require('./switchToSDLSchema'); const ensureIntrospectionSchema = require('./ensureIntrospectionSchema'); const { readSchemaFromFile } = require('./readSchemaFromFile'); -const defaultDirectiveDefinitions = require('./defaultDirectiveDefinitions'); const getRelativeTypesPath = require('./getRelativeTypesPath'); module.exports = { getAppSyncAPIDetails, @@ -37,6 +36,5 @@ module.exports = { switchToSDLSchema, ensureIntrospectionSchema, readSchemaFromFile, - defaultDirectiveDefinitions, getRelativeTypesPath, }; diff --git a/packages/amplify-codegen/src/walkthrough/questions/languageTarget.js b/packages/amplify-codegen/src/walkthrough/questions/languageTarget.js index c1457632e..f120868a3 100644 --- a/packages/amplify-codegen/src/walkthrough/questions/languageTarget.js +++ b/packages/amplify-codegen/src/walkthrough/questions/languageTarget.js @@ -43,6 +43,7 @@ async function askCodeGenTargetLanguage(context, target, withoutInit = false, de default: target || null, }, ]); + return answer.target; } diff --git a/packages/amplify-codegen/tests/commands/models.test.js b/packages/amplify-codegen/tests/commands/models.test.js index 844699adf..00868566c 100644 --- a/packages/amplify-codegen/tests/commands/models.test.js +++ b/packages/amplify-codegen/tests/commands/models.test.js @@ -3,7 +3,7 @@ const { validateAmplifyFlutterMinSupportedVersion, MINIMUM_SUPPORTED_VERSION_CONSTRAINT, } = require('../../src/utils/validateAmplifyFlutterMinSupportedVersion'); -const defaultDirectiveDefinitions = require('../../src/utils/defaultDirectiveDefinitions'); +const { DefaultDirectives } = require('@aws-amplify/graphql-directives'); const mockFs = require('mock-fs'); const fs = require('fs'); const path = require('path'); @@ -308,6 +308,6 @@ function addMocksToContext() { }, ], }); - MOCK_CONTEXT.amplify.executeProviderUtils.mockReturnValue(defaultDirectiveDefinitions); + MOCK_CONTEXT.amplify.executeProviderUtils.mockReturnValue(DefaultDirectives.map(directive => directive.definition).join('\n')); MOCK_CONTEXT.amplify.pathManager.getBackendDirPath.mockReturnValue(MOCK_BACKEND_DIRECTORY); } diff --git a/packages/amplify-codegen/tests/commands/types.test.js b/packages/amplify-codegen/tests/commands/types.test.js index 044cdc19f..37e17cfdb 100644 --- a/packages/amplify-codegen/tests/commands/types.test.js +++ b/packages/amplify-codegen/tests/commands/types.test.js @@ -12,6 +12,7 @@ const { ensureIntrospectionSchema, getFrontEndHandler, getAppSyncAPIDetails } = const MOCK_CONTEXT = { print: { info: jest.fn(), + warning: jest.fn(), }, amplify: { getEnvInfo: jest.fn(), @@ -56,6 +57,19 @@ const MOCK_APIS = [ getFrontEndHandler.mockReturnValue('javascript'); +const MOCK_ANGULAR_PROJECT_BASE = { + excludes: [MOCK_EXCLUDE_PATH], + includes: [MOCK_INCLUDE_PATH], + schema: MOCK_SCHEMA, + amplifyExtension: { + generatedFileName: MOCK_GENERATED_FILE_NAME, + codeGenTarget: 'angular', + graphQLApiId: MOCK_API_ID, + region: MOCK_REGION, + amplifyJsLibraryVersion: 5, + }, +}; + describe('command - types', () => { beforeEach(() => { jest.clearAllMocks(); @@ -163,4 +177,30 @@ describe('command - types', () => { expect(generateTypesHelper).not.toHaveBeenCalled(); expect(globby.sync).not.toHaveBeenCalled(); }); + + it('should show a warning if the amplifyJsLibraryVersion is invalid', async () => { + const MOCK_ANGULAR_PROJECT = { + ...MOCK_ANGULAR_PROJECT_BASE + }; + MOCK_ANGULAR_PROJECT.amplifyExtension.amplifyJsLibraryVersion = 7 + fs.readFileSync + .mockReturnValueOnce('query 1') + .mockReturnValueOnce('query 2') + .mockReturnValueOnce('schema'); + loadConfig.mockReturnValue({ + getProjects: jest.fn().mockReturnValue([MOCK_ANGULAR_PROJECT]), + }); + await generateTypes(MOCK_CONTEXT, false); + expect(MOCK_CONTEXT.print.warning).toHaveBeenCalledWith( + 'Amplify JS library version 7 is not supported. The current support JS library version is [5, 6]. Codegen will be executed for JS v6 instead.' + ); + expect(generateTypesHelper).toHaveBeenCalledWith({ + queries: [new Source('query 1', 'q1.gql'), new Source('query 2', 'q2.gql')], + schema: 'schema', + target: 'angular', + introspection: false, + multipleSwiftFiles: false, + amplifyJsLibraryVersion: 6, + }); + }); }); diff --git a/packages/appsync-modelgen-plugin/API.md b/packages/appsync-modelgen-plugin/API.md index 803cb2dc2..f9b62d480 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/CHANGELOG.md b/packages/appsync-modelgen-plugin/CHANGELOG.md index eb5e9e90f..1f2c4d5a4 100644 --- a/packages/appsync-modelgen-plugin/CHANGELOG.md +++ b/packages/appsync-modelgen-plugin/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.10.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/appsync-modelgen-plugin@2.9.0...@aws-amplify/appsync-modelgen-plugin@2.10.0) (2024-04-03) + +### Bug Fixes + +- process input object, union and interface metadata in model introspection schema codegen ([#795](https://github.com/aws-amplify/amplify-codegen/issues/795)) ([73e4520](https://github.com/aws-amplify/amplify-codegen/commit/73e4520e8f3bbd63d6b123a5c977c415df443905)) + +### Features + +- use default directives from @aws-amplify/graphql-directives ([#796](https://github.com/aws-amplify/amplify-codegen/issues/796)) ([a94649e](https://github.com/aws-amplify/amplify-codegen/commit/a94649ef5cbed1091e4c206852d85f4b860a3eae)) + # [2.9.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/appsync-modelgen-plugin@2.8.1...@aws-amplify/appsync-modelgen-plugin@2.9.0) (2024-01-29) ### Features diff --git a/packages/appsync-modelgen-plugin/package.json b/packages/appsync-modelgen-plugin/package.json index 6ac3d9a35..c333a6cab 100644 --- a/packages/appsync-modelgen-plugin/package.json +++ b/packages/appsync-modelgen-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/appsync-modelgen-plugin", - "version": "2.9.0", + "version": "2.10.0", "repository": { "type": "git", "url": "https://github.com/aws-amplify/amplify-codegen.git", @@ -39,6 +39,7 @@ "ts-dedent": "^1.1.0" }, "devDependencies": { + "@aws-amplify/graphql-directives": "^1.0.1", "@graphql-codegen/testing": "^1.17.7", "@graphql-codegen/typescript": "^2.8.3", "@types/fs-extra": "^8.1.2", diff --git a/packages/appsync-modelgen-plugin/src/__tests__/utils/process-connections-v2.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/utils/process-connections-v2.test.ts index 923c0772c..4f8fb7d72 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/utils/process-connections-v2.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/utils/process-connections-v2.test.ts @@ -1,3 +1,4 @@ +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; import { CodeGenModelMap, CodeGenModel, CodeGenField } from '../../visitors/appsync-visitor'; import { processConnectionsV2 } from '../../utils/process-connections-v2'; import { @@ -7,7 +8,7 @@ import { CodeGenFieldConnectionHasOne, } from '../../utils/process-connections'; import { buildSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelVisitor, CodeGenGenerateEnum } from '../../visitors/appsync-visitor'; describe('GraphQL V2 process connections tests', () => { @@ -636,6 +637,7 @@ describe('Connection process with custom Primary Key support tests', () => { transformerVersion: 2 }; const ast = parse(schema); + const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); const builtSchema = buildSchema([schema, directives, scalars].join('\n')) const visitor = new AppSyncModelVisitor( builtSchema, @@ -1033,4 +1035,4 @@ describe('Connection process with custom Primary Key support tests', () => { }); }); }) -}); \ No newline at end of file +}); 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 b94c5399c..e7fd2efee 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 000000000..f7e1ca2a8 --- /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-dart-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-dart-visitor.test.ts index f77dcf1ff..b51eefd58 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-dart-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-dart-visitor.test.ts @@ -1,10 +1,11 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelDartVisitor } from '../../visitors/appsync-dart-visitor'; import { CodeGenGenerateEnum } from '../../visitors/appsync-visitor'; import { DART_SCALAR_MAP } from '../../scalars'; -const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { +const buildSchemaWithDirectives = (schema: String, directives: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; @@ -16,6 +17,7 @@ const getVisitor = ({ transformerVersion = 1, dartUpdateAmplifyCoreDependency = false, respectPrimaryKeyAttributesOnConnectionField = false, + directives = DefaultDirectives, }: { schema: string; selectedType?: string; @@ -24,13 +26,15 @@ const getVisitor = ({ transformerVersion?: number; dartUpdateAmplifyCoreDependency?: boolean; respectPrimaryKeyAttributesOnConnectionField?: boolean; + directives?: readonly Directive[]; }) => { const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncModelDartVisitor( builtSchema, { - directives, + directives: stringDirectives, target: 'dart', scalars: DART_SCALAR_MAP, isTimestampFieldsAdded, @@ -85,7 +89,7 @@ describe('AppSync Dart Visitor', () => { book: String } `; - const visitor = getVisitor({ schema }); + const visitor = getVisitor({ schema, directives: [...AppSyncDirectives, ...V1Directives, DeprecatedDirective] }); const generatedCode = visitor.generate(); expect(generatedCode).toMatchSnapshot(); }); @@ -272,7 +276,7 @@ describe('AppSync Dart Visitor', () => { `; const outputModels: string[] = ['Todo', 'Task']; outputModels.forEach(model => { - const generatedCode = getVisitor({schema, selectedType: model}).generate(); + const generatedCode = getVisitor({schema, selectedType: model, directives: [...AppSyncDirectives, ...V1Directives, DeprecatedDirective] }).generate(); expect(generatedCode).toMatchSnapshot(); }); }); @@ -301,7 +305,7 @@ describe('AppSync Dart Visitor', () => { `; const outputModels: string[] = ['Blog', 'Comment', 'Post']; outputModels.forEach(model => { - const generatedCode = getVisitor({ schema, selectedType: model }).generate(); + const generatedCode = getVisitor({ schema, selectedType: model, directives: [...AppSyncDirectives, ...V1Directives, DeprecatedDirective] }).generate(); expect(generatedCode).toMatchSnapshot(); }); }); @@ -457,7 +461,7 @@ describe('AppSync Dart Visitor', () => { `; const outputModels: string[] = ['Blog', 'Comment', 'Post']; outputModels.forEach(model => { - const generatedCode = getVisitor({ schema, selectedType: model }).generate(); + const generatedCode = getVisitor({ schema, selectedType: model, directives: [...AppSyncDirectives, ...V1Directives, DeprecatedDirective] }).generate(); expect(generatedCode).toMatchSnapshot(); }); }); diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-api-lazyload-css-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-api-lazyload-css-visitor.test.ts index 2cc7989ca..98de3fc2b 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-api-lazyload-css-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-api-lazyload-css-visitor.test.ts @@ -1,6 +1,7 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; import { validateJava } from '../utils/validate-java'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelJavaVisitor } from '../../visitors/appsync-java-visitor'; import { CodeGenGenerateEnum } from '../../visitors/appsync-visitor'; import { JAVA_SCALAR_MAP } from '../../scalars'; @@ -13,6 +14,9 @@ const defaultJavaVisitorSettings = { respectPrimaryKeyAttributesOnConnectionField: false, generateModelsForLazyLoadAndCustomSelectionSet: true } + +const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts index cf748940f..39a9b839b 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-java-visitor.test.ts @@ -1,6 +1,7 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; import { validateJava } from '../utils/validate-java'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelJavaVisitor } from '../../visitors/appsync-java-visitor'; import { CodeGenGenerateEnum } from '../../visitors/appsync-visitor'; import { JAVA_SCALAR_MAP } from '../../scalars'; @@ -12,18 +13,20 @@ const defaultJavaVisitorSettings = { generate: CodeGenGenerateEnum.code, respectPrimaryKeyAttributesOnConnectionField: false, }; -const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { + +const buildSchemaWithDirectives = (schema: String, directives: string): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; -const getVisitor = (schema: string, selectedType?: string, settings: any = {}) => { +const getVisitor = (schema: string, selectedType?: string, settings: any = {}, directives: readonly Directive[] = DefaultDirectives) => { const visitorConfig = { ...defaultJavaVisitorSettings, ...settings }; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncModelJavaVisitor( builtSchema, { - directives, + directives: stringDirectives, target: 'java', scalars: JAVA_SCALAR_MAP, ...visitorConfig, @@ -102,7 +105,7 @@ describe('AppSyncModelVisitor', () => { } `; - const visitor = getVisitor(schema, 'SimpleModel'); + const visitor = getVisitor(schema, 'SimpleModel', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); @@ -200,7 +203,7 @@ describe('AppSyncModelVisitor', () => { book: String } `; - const visitor = getVisitor(schema, 'authorBook'); + const visitor = getVisitor(schema, 'authorBook', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); @@ -250,7 +253,7 @@ describe('AppSyncModelVisitor', () => { book: String } `; - const visitorV1 = getVisitor(schemaV1, 'authorBook'); + const visitorV1 = getVisitor(schemaV1, 'authorBook', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const visitorV2 = getVisitorPipelinedTransformer(schemaV2, 'authorBook'); const version1Code = visitorV1.generate(); const version2Code = visitorV2.generate(); @@ -277,7 +280,7 @@ describe('AppSyncModelVisitor', () => { book: String } `; - const visitorV1 = getVisitor(schemaV1, 'authorBook'); + const visitorV1 = getVisitor(schemaV1, 'authorBook', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const visitorV2 = getVisitorPipelinedTransformer(schemaV2, 'authorBook'); const version1Code = visitorV1.generate(); const version2Code = visitorV2.generate(); @@ -558,14 +561,14 @@ describe('AppSyncModelVisitor', () => { } `; it('should generate one side of the connection', () => { - const visitor = getVisitor(schema, 'Todo'); + const visitor = getVisitor(schema, 'Todo', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); }); it('should generate many side of the connection', () => { - const visitor = getVisitor(schema, 'task'); + const visitor = getVisitor(schema, 'task', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); @@ -588,14 +591,14 @@ describe('AppSyncModelVisitor', () => { } `; it('should generate class for one side of the connection', () => { - const visitor = getVisitor(schema, 'Todo'); + const visitor = getVisitor(schema, 'Todo', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); }); it('should generate class for many side of the connection', () => { - const visitor = getVisitor(schema, 'task'); + const visitor = getVisitor(schema, 'task', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(() => validateJava(generatedCode)).not.toThrow(); expect(generatedCode).toMatchSnapshot(); diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-javascript-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-javascript-visitor.test.ts index 1702b6f59..13c1420be 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-javascript-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-javascript-visitor.test.ts @@ -1,10 +1,12 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; import { validateTs } from '@graphql-codegen/testing'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; import { TYPESCRIPT_SCALAR_MAP } from '../../scalars'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelJavascriptVisitor } from '../../visitors/appsync-javascript-visitor'; -const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { + +const buildSchemaWithDirectives = (schema: String, directives: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; export type JavaScriptVisitorConfig = { @@ -19,13 +21,14 @@ const defaultJavaScriptVisitorConfig: JavaScriptVisitorConfig = { respectPrimaryKeyAttributesOnConnectionField: false, transformerVersion: 1, }; -const getVisitor = (schema: string, settings: JavaScriptVisitorConfig = {}): AppSyncModelJavascriptVisitor => { +const getVisitor = (schema: string, settings: JavaScriptVisitorConfig = {}, directives: readonly Directive[] = DefaultDirectives): AppSyncModelJavascriptVisitor => { const config = { ...defaultJavaScriptVisitorConfig, ...settings }; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncModelJavascriptVisitor( builtSchema, - { directives, target: 'javascript', scalars: TYPESCRIPT_SCALAR_MAP, ...config }, + { directives: stringDirectives, target: 'javascript', scalars: TYPESCRIPT_SCALAR_MAP, ...config }, {}, ); visit(ast, { leave: visitor }); @@ -56,7 +59,7 @@ describe('Javascript visitor', () => { `; let visitor: AppSyncModelJavascriptVisitor; beforeEach(() => { - visitor = getVisitor(schema); + visitor = getVisitor(schema, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); }); describe('enums', () => { @@ -101,7 +104,7 @@ describe('Javascript visitor', () => { }); it('should generate Javascript declaration', () => { - const declarationVisitor = getVisitor(schema, { isDeclaration: true }); + const declarationVisitor = getVisitor(schema, { isDeclaration: true }, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generateImportSpy = jest.spyOn(declarationVisitor as any, 'generateImports'); const generateEnumDeclarationsSpy = jest.spyOn(declarationVisitor as any, 'generateEnumDeclarations'); const generateModelDeclarationSpy = jest.spyOn(declarationVisitor as any, 'generateModelDeclaration'); @@ -189,7 +192,7 @@ describe('Javascript visitor', () => { }); it('should generate Javascript declaration with model metadata types', () => { - const declarationVisitor = getVisitor(schema, { isDeclaration: true, isTimestampFieldsAdded: true }); + const declarationVisitor = getVisitor(schema, { isDeclaration: true, isTimestampFieldsAdded: true }, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generateImportSpy = jest.spyOn(declarationVisitor as any, 'generateImports'); const generateEnumDeclarationsSpy = jest.spyOn(declarationVisitor as any, 'generateEnumDeclarations'); const generateModelDeclarationSpy = jest.spyOn(declarationVisitor as any, 'generateModelDeclaration'); @@ -290,7 +293,7 @@ describe('Javascript visitor', () => { }); it('should generate Javascript code when declaration is set to false', () => { - const jsVisitor = getVisitor(schema); + const jsVisitor = getVisitor(schema, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generateImportsJavaScriptImplementationSpy = jest.spyOn(jsVisitor as any, 'generateImportsJavaScriptImplementation'); const generateEnumObjectSpy = jest.spyOn(jsVisitor as any, 'generateEnumObject'); const generateModelInitializationSpy = jest.spyOn(jsVisitor as any, 'generateModelInitialization'); diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-json-metadata-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-json-metadata-visitor.test.ts index 43c7ee337..94b1d0314 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-json-metadata-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-json-metadata-visitor.test.ts @@ -1,6 +1,7 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; import { TYPESCRIPT_SCALAR_MAP } from '../../scalars'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { CodeGenConnectionType, CodeGenFieldConnectionBelongsTo, @@ -10,13 +11,14 @@ import { import { AppSyncJSONVisitor, AssociationHasMany, JSONSchemaNonModel } from '../../visitors/appsync-json-metadata-visitor'; import { CodeGenEnum, CodeGenField, CodeGenModel } from '../../visitors/appsync-visitor'; + const defaultJSONVisitorSettings = { isTimestampFieldsAdded: true, respectPrimaryKeyAttributesOnConnectionField: false, transformerVersion: 1, }; -const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { +const buildSchemaWithDirectives = (schema: String, directives: string): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; @@ -24,13 +26,15 @@ const getVisitor = ( schema: string, target: 'typescript' | 'javascript' | 'typeDeclaration' = 'javascript', settings: any = {}, + directives: readonly Directive[] = DefaultDirectives, ): AppSyncJSONVisitor => { const visitorConfig = { ...defaultJSONVisitorSettings, ...settings }; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncJSONVisitor( builtSchema, - { directives, target: 'metadata', scalars: TYPESCRIPT_SCALAR_MAP, metadataTarget: target, codegenVersion: '1.0.0', ...visitorConfig }, + { directives: stringDirectives, target: 'metadata', scalars: TYPESCRIPT_SCALAR_MAP, metadataTarget: target, codegenVersion: '1.0.0', ...visitorConfig }, {}, ); visit(ast, { leave: visitor }); @@ -1103,11 +1107,11 @@ describe('Metadata visitor has one relation', () => { `; let visitor: AppSyncJSONVisitor; beforeEach(() => { - visitor = getVisitor(schema); + visitor = getVisitor(schema, 'javascript', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); }); it('should generate for Javascript', () => { - const jsVisitor = getVisitor(schema, 'javascript'); + const jsVisitor = getVisitor(schema, 'javascript', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(jsVisitor.generate()).toMatchInlineSnapshot(` "export const schema = { \\"models\\": { @@ -1228,7 +1232,7 @@ describe('Metadata visitor has one relation', () => { }); it('should generate for TypeScript', () => { - const tsVisitor = getVisitor(schema, 'typescript'); + const tsVisitor = getVisitor(schema, 'typescript', {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(tsVisitor.generate()).toMatchInlineSnapshot(` "import { Schema } from \\"@aws-amplify/datastore\\"; 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 dcb9d0e62..98c2423d6 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 @@ -1,6 +1,7 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; import { METADATA_SCALAR_MAP } from '../../scalars'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelIntrospectionVisitor } from '../../visitors/appsync-model-introspection-visitor'; const defaultModelIntropectionVisitorSettings = { @@ -9,17 +10,18 @@ const defaultModelIntropectionVisitorSettings = { transformerVersion: 2 } -const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { +const buildSchemaWithDirectives = (schema: String, directives: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; -const getVisitor = (schema: string, settings: any = {}): AppSyncModelIntrospectionVisitor => { +const getVisitor = (schema: string, settings: any = {}, directives: readonly Directive[] = DefaultDirectives): AppSyncModelIntrospectionVisitor => { const visitorConfig = { ...defaultModelIntropectionVisitorSettings, ...settings } const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncModelIntrospectionVisitor( builtSchema, - { directives, scalars: METADATA_SCALAR_MAP, ...visitorConfig, target: 'introspection' }, + { directives: stringDirectives, scalars: METADATA_SCALAR_MAP, ...visitorConfig, target: 'introspection' }, {}, ); visit(ast, { leave: visitor }); @@ -51,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', () => { @@ -66,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'); }); @@ -290,7 +311,8 @@ describe('schemas with pk on a belongsTo fk', () => { `, { transformerVersion: 1, usePipelinedTransformer: false, - }).generate()).toMatchSnapshot(); + }, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]).generate()).toMatchSnapshot(); + }); it('works for v2', () => { @@ -315,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-swift-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-swift-visitor.test.ts index c3109f72d..a59eccfbf 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-swift-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-swift-visitor.test.ts @@ -1,5 +1,6 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { SWIFT_SCALAR_MAP } from '../../scalars'; import { AppSyncSwiftVisitor } from '../../visitors/appsync-swift-visitor'; import { CodeGenGenerateEnum } from '../../visitors/appsync-visitor'; @@ -14,7 +15,8 @@ const defaultIosVisitorSetings = { improvePluralization: true, generateModelsForLazyLoadAndCustomSelectionSet: true, }; -const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { + +const buildSchemaWithDirectives = (schema: String, directives: string): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; @@ -23,14 +25,16 @@ const getVisitor = ( selectedType?: string, generate: CodeGenGenerateEnum = CodeGenGenerateEnum.code, settings: any = {}, + directives: readonly Directive[] = DefaultDirectives, ) => { const visitorConfig = { ...defaultIosVisitorSetings, ...settings }; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncSwiftVisitor( builtSchema, { - directives, + directives: stringDirectives, target: 'swift', scalars: SWIFT_SCALAR_MAP, ...visitorConfig, @@ -328,7 +332,7 @@ describe('AppSyncSwiftVisitor', () => { book: String } `; - const visitor = getVisitor(schema, 'authorBook'); + const visitor = getVisitor(schema, 'authorBook', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(generatedCode).toMatchInlineSnapshot(` "// swiftlint:disable all @@ -375,7 +379,7 @@ describe('AppSyncSwiftVisitor', () => { }" `); - const metadataVisitor = getVisitor(schema, 'authorBook', CodeGenGenerateEnum.metadata); + const metadataVisitor = getVisitor(schema, 'authorBook', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedMetadata = metadataVisitor.generate(); expect(generatedMetadata).toMatchInlineSnapshot(` "// swiftlint:disable all @@ -467,14 +471,14 @@ describe('AppSyncSwiftVisitor', () => { book: String } `; - const visitorV1 = getVisitor(schemaV1, 'authorBook'); + const visitorV1 = getVisitor(schemaV1, 'authorBook', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const visitorV2 = getVisitorPipelinedTransformer(schemaV2, 'authorBook'); const version1Code = visitorV1.generate(); const version2Code = visitorV2.generate(); expect(version1Code).toMatch(version2Code); - const metadataVisitorV1 = getVisitor(schemaV1, 'authorBook', CodeGenGenerateEnum.metadata); + const metadataVisitorV1 = getVisitor(schemaV1, 'authorBook', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const metadataVisitorV2 = getVisitorPipelinedTransformer(schemaV2, 'authorBook', CodeGenGenerateEnum.metadata); const version1Metadata = metadataVisitorV1.generate(); const version2Metadata = metadataVisitorV2.generate(); @@ -501,14 +505,14 @@ describe('AppSyncSwiftVisitor', () => { book: String } `; - const visitorV1 = getVisitor(schemaV1, 'authorBook'); + const visitorV1 = getVisitor(schemaV1, 'authorBook', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const visitorV2 = getVisitorPipelinedTransformer(schemaV2, 'authorBook'); const version1Code = visitorV1.generate(); const version2Code = visitorV2.generate(); expect(version1Code).toMatch(version2Code); - const metadataVisitorV1 = getVisitor(schemaV1, 'authorBook', CodeGenGenerateEnum.metadata); + const metadataVisitorV1 = getVisitor(schemaV1, 'authorBook', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const metadataVisitorV2 = getVisitorPipelinedTransformer(schemaV2, 'authorBook', CodeGenGenerateEnum.metadata); const version1Metadata = metadataVisitorV1.generate(); const version2Metadata = metadataVisitorV2.generate(); @@ -575,18 +579,18 @@ describe('AppSyncSwiftVisitor', () => { } `; - const visitor = getVisitor(schema, 'Todo'); + const visitor = getVisitor(schema, 'Todo', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(generatedCode).toMatchSnapshot(); - const metadataVisitor = getVisitor(schema, 'Todo', CodeGenGenerateEnum.metadata); + const metadataVisitor = getVisitor(schema, 'Todo', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedMetadata = metadataVisitor.generate(); expect(generatedMetadata).toMatchSnapshot(); - const taskVisitor = getVisitor(schema, 'task'); + const taskVisitor = getVisitor(schema, 'task', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(taskVisitor.generate()).toMatchSnapshot(); - const taskMetadataVisitor = getVisitor(schema, 'task', CodeGenGenerateEnum.metadata); + const taskMetadataVisitor = getVisitor(schema, 'task', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedTaskMetadata = taskMetadataVisitor.generate(); expect(generatedTaskMetadata).toMatchSnapshot(); }); @@ -615,7 +619,7 @@ describe('AppSyncSwiftVisitor', () => { } `; it('should generate one side of the connection', () => { - const visitor = getVisitor(schema, 'Todo'); + const visitor = getVisitor(schema, 'Todo', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(generatedCode).toMatchInlineSnapshot(` "// swiftlint:disable all @@ -677,7 +681,7 @@ describe('AppSyncSwiftVisitor', () => { }" `); - const metadataVisitor = getVisitor(schema, 'Todo', CodeGenGenerateEnum.metadata); + const metadataVisitor = getVisitor(schema, 'Todo', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedMetadata = metadataVisitor.generate(); expect(generatedMetadata).toMatchInlineSnapshot(` "// swiftlint:disable all @@ -761,7 +765,7 @@ describe('AppSyncSwiftVisitor', () => { }); it('should generate many side of the connection', () => { - const visitor = getVisitor(schema, 'task'); + const visitor = getVisitor(schema, 'task', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedCode = visitor.generate(); expect(generatedCode).toMatchInlineSnapshot(` "// swiftlint:disable all @@ -843,7 +847,7 @@ describe('AppSyncSwiftVisitor', () => { }" `); - const metadataVisitor = getVisitor(schema, 'task', CodeGenGenerateEnum.metadata); + const metadataVisitor = getVisitor(schema, 'task', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); const generatedMetadata = metadataVisitor.generate(); expect(generatedMetadata).toMatchInlineSnapshot(` "// swiftlint:disable all @@ -945,7 +949,7 @@ describe('AppSyncSwiftVisitor', () => { posts: [PostEditor] @connection(keyName: "byEditor", fields: ["id"]) } `; - const postVisitor = getVisitor(schema, 'Post'); + const postVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(() => postVisitor.generate()).not.toThrowError(); }); @@ -977,7 +981,7 @@ describe('AppSyncSwiftVisitor', () => { } `; - const postVisitor = getVisitor(schema, 'Post'); + const postVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(postVisitor.generate()).toMatchInlineSnapshot(` "// swiftlint:disable all import Amplify @@ -1013,7 +1017,7 @@ describe('AppSyncSwiftVisitor', () => { }" `); - const postSchemaVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.metadata); + const postSchemaVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(postSchemaVisitor.generate()).toMatchInlineSnapshot(` "// swiftlint:disable all import Amplify @@ -1069,7 +1073,7 @@ describe('AppSyncSwiftVisitor', () => { }" `); - const postEditorVisitor = getVisitor(schema, 'Post'); + const postEditorVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.code, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(postEditorVisitor.generate()).toMatchInlineSnapshot(` "// swiftlint:disable all import Amplify @@ -1105,7 +1109,7 @@ describe('AppSyncSwiftVisitor', () => { }" `); - const postEditorSchemaVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.metadata); + const postEditorSchemaVisitor = getVisitor(schema, 'Post', CodeGenGenerateEnum.metadata, {}, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); expect(postEditorSchemaVisitor.generate()).toMatchInlineSnapshot(` "// swiftlint:disable all import Amplify diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts index 3b53acb0f..fe80c6499 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts @@ -1,9 +1,12 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; import { validateTs } from '@graphql-codegen/testing'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; import { TYPESCRIPT_SCALAR_MAP } from '../../scalars'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { AppSyncModelTypeScriptVisitor } from '../../visitors/appsync-typescript-visitor'; +const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; 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 7560769e2..2f5fedc0e 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 @@ -1,5 +1,6 @@ import { buildSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../scalars/supported-directives'; +import { AppSyncDirectives, DefaultDirectives, V1Directives, DeprecatedDirective, Directive } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../scalars/supported-scalars'; import { CodeGenConnectionType, CodeGenFieldConnectionBelongsTo, CodeGenFieldConnectionHasMany, CodeGenFieldConnectionHasOne } from '../../utils/process-connections'; import { AppSyncModelVisitor, CodeGenGenerateEnum, CodeGenPrimaryKeyType } from '../../visitors/appsync-visitor'; @@ -8,17 +9,19 @@ const defaultBaseVisitorSettings = { isTimestampFieldsAdded: true, respectPrimaryKeyAttributesOnConnectionField: false } -const buildSchemaWithDirectives = (schema: String) => { + +const buildSchemaWithDirectives = (schema: String, directives: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; -const createAndGenerateVisitor = (schema: string, settings: any = {}) => { +const createAndGenerateVisitor = (schema: string, settings: any = {}, directives: readonly Directive[] = DefaultDirectives) => { const visitorConfig = {...defaultBaseVisitorSettings, ...settings} const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = directives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncModelVisitor( builtSchema, - { directives, target: 'general', ...visitorConfig }, + { directives: stringDirectives, target: 'general', ...visitorConfig }, { generate: CodeGenGenerateEnum.code }, ); visit(ast, { leave: visitor }); @@ -40,8 +43,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); expect(visitor.models.Post).toBeDefined(); @@ -61,8 +65,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'general', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'general', generate: CodeGenGenerateEnum.code }, {}); expect(() => visit(ast, { leave: visitor })).not.toThrowError(); }); it('should change field to non-nullable when schema has id of nullable type', () => { @@ -86,8 +91,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); const postFields = visitor.models.Post.fields; expect(postFields[0].name).toEqual('id'); @@ -206,8 +212,9 @@ describe('AppSyncModelVisitor', () => { `; it('one to many connection', () => { const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = [...AppSyncDirectives, ...V1Directives, DeprecatedDirective].map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const commentsField = visitor.models.Post.fields.find(f => f.name === 'comments'); @@ -222,8 +229,9 @@ describe('AppSyncModelVisitor', () => { it('many to one connection', () => { const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = [...AppSyncDirectives, ...V1Directives, DeprecatedDirective].map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const commentsField = visitor.models.Post.fields.find(f => f.name === 'comments'); @@ -254,8 +262,9 @@ describe('AppSyncModelVisitor', () => { it('one to many connection', () => { const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = [...AppSyncDirectives, ...V1Directives, DeprecatedDirective].map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const commentsField = visitor.models.Post.fields.find(f => f.name === 'comments'); @@ -271,8 +280,9 @@ describe('AppSyncModelVisitor', () => { it('many to one connection', () => { const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = [...AppSyncDirectives, ...V1Directives, DeprecatedDirective].map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); @@ -303,8 +313,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = [...AppSyncDirectives, ...V1Directives, DeprecatedDirective].map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const postFields = visitor.models.Post.fields.map(field => field.name); @@ -327,8 +338,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = [...AppSyncDirectives, ...V1Directives, DeprecatedDirective].map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const commentsField = visitor.models.Comment.fields.map(field => field.name); @@ -349,10 +361,11 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); const visitor = new AppSyncModelVisitor( builtSchema, - { directives, target: 'typescript', generate: CodeGenGenerateEnum.code, usePipelinedTransformer: true }, + { directives: stringDirectives, target: 'typescript', generate: CodeGenGenerateEnum.code, usePipelinedTransformer: true }, {}, ); visit(ast, { leave: visitor }); @@ -461,8 +474,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const postModel = visitor.models.Post; @@ -489,8 +503,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); const postModel = visitor.models.Post; @@ -520,8 +535,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - const visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + const visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); @@ -551,8 +567,9 @@ describe('AppSyncModelVisitor', () => { } `; const ast = parse(schema); - const builtSchema = buildSchemaWithDirectives(schema); - visitor = new AppSyncModelVisitor(builtSchema, { directives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); + const stringDirectives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const builtSchema = buildSchemaWithDirectives(schema, stringDirectives); + visitor = new AppSyncModelVisitor(builtSchema, { directives: stringDirectives, target: 'android', generate: CodeGenGenerateEnum.code }, {}); visit(ast, { leave: visitor }); visitor.generate(); }); @@ -1061,7 +1078,7 @@ describe('AppSyncModelVisitor', () => { id: ID! } `; - const { models, nonModels } = createAndGenerateVisitor(schemaV1, { respectPrimaryKeyAttributesOnConnectionField: true }); + const { models, nonModels } = createAndGenerateVisitor(schemaV1, { respectPrimaryKeyAttributesOnConnectionField: true }, [...AppSyncDirectives, ...V1Directives, DeprecatedDirective]); it('should have id field as primary key when no custom PK defined', () => { const primaryKeyField = models.WorkItem0.fields.find(field => field.name === 'id')!; expect(primaryKeyField).toBeDefined(); @@ -1322,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/__tests__/visitors/gqlv2-regression-tests/appsync-dart-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-dart-visitor.test.ts index 3adcea785..4fa3964cf 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-dart-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-dart-visitor.test.ts @@ -1,9 +1,12 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../../scalars/supported-directives'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../../scalars/supported-scalars'; import { AppSyncModelDartVisitor } from '../../../visitors/appsync-dart-visitor'; import { CodeGenGenerateEnum } from '../../../visitors/appsync-visitor'; import { DART_SCALAR_MAP } from '../../../scalars'; +const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-java-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-java-visitor.test.ts index 22bf45d28..dcb06104f 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-java-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-java-visitor.test.ts @@ -1,9 +1,12 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../../scalars/supported-directives'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../../scalars/supported-scalars'; import { JAVA_SCALAR_MAP } from '../../../scalars'; import { AppSyncModelJavaVisitor } from '../../../visitors/appsync-java-visitor'; import { CodeGenGenerateEnum } from '../../../visitors/appsync-visitor'; +const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-javascript-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-javascript-visitor.test.ts index 429be1c8a..00389daa5 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-javascript-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-javascript-visitor.test.ts @@ -1,9 +1,12 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../../scalars/supported-directives'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../../scalars/supported-scalars'; import { TYPESCRIPT_SCALAR_MAP } from '../../../scalars'; import { AppSyncModelJavascriptVisitor } from '../../../visitors/appsync-javascript-visitor'; import { JavaScriptVisitorConfig } from '../appsync-javascript-visitor.test'; +const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-swift-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-swift-visitor.test.ts index 77973cb8a..f646c6b31 100644 --- a/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-swift-visitor.test.ts +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/gqlv2-regression-tests/appsync-swift-visitor.test.ts @@ -1,9 +1,12 @@ import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; -import { directives, scalars } from '../../../scalars/supported-directives'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; +import { scalars } from '../../../scalars/supported-scalars'; import { SWIFT_SCALAR_MAP } from '../../../scalars'; import { AppSyncSwiftVisitor } from '../../../visitors/appsync-swift-visitor'; import { CodeGenGenerateEnum } from '../../../visitors/appsync-visitor'; +const directives = DefaultDirectives.map(directive => directive.definition).join('\n'); + const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { return buildSchema([schema, directives, scalars].join('\n')); }; 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 aea3f33b3..eba55bcab 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/scalars/supported-directives.ts b/packages/appsync-modelgen-plugin/src/scalars/supported-directives.ts deleted file mode 100644 index b7f5f9056..000000000 --- a/packages/appsync-modelgen-plugin/src/scalars/supported-directives.ts +++ /dev/null @@ -1,165 +0,0 @@ -// Used only in tests. Directive definition will be passed as part of the configuration when modelgen is run using CLI -// TODO remove once prettier is upgraded -// prettier-ignore -export const directives = /* GraphQL */ ` - # model directive - directive @model( - queries: ModelQueryMap - mutations: ModelMutationMap - subscriptions: ModelSubscriptionMap - timestamps: TimestampConfiguration - ) on OBJECT - input ModelMutationMap { - create: String - update: String - delete: String - } - input ModelQueryMap { - get: String - list: String - } - input ModelSubscriptionMap { - onCreate: [String] - onUpdate: [String] - onDelete: [String] - level: ModelSubscriptionLevel - } - enum ModelSubscriptionLevel { - off - public - on - } - input TimestampConfiguration { - createdAt: String - updatedAt: String - } - - # Key directive - directive @key(name: String, fields: [String!]!, queryField: String) repeatable on OBJECT - - # Connection directive - directive @connection( - name: String - keyField: String - sortField: String - keyName: String - limit: Int - fields: [String!] - ) on FIELD_DEFINITION - - directive @auth(rules: [AuthRule!]!) on OBJECT | FIELD_DEFINITION - - input AuthRule { - # Specifies the auth rule's strategy. Allowed values are 'owner', 'groups', 'public', 'private'. - allow: AuthStrategy! - - # Legacy name for identityClaim - identityField: String @deprecated(reason: "The 'identityField' argument is replaced by the 'identityClaim'.") - - # Specifies the name of the provider to use for the rule. This overrides the default provider - # when 'public' and 'private' AuthStrategy is used. Specifying a provider for 'owner' or 'groups' - # are not allowed. - provider: AuthProvider - - # Specifies the name of the claim to look for on the request's JWT token - # from Cognito User Pools (and in the future OIDC) that contains the identity - # of the user. If 'allow' is 'groups', this value should point to a list of groups - # in the claims. If 'allow' is 'owner', this value should point to the logged in user identity string. - # Defaults to "cognito:username" for Cognito User Pools auth. - identityClaim: String - - # Allows for custom config of 'groups' which is validated against the JWT - # Specifies a static list of groups that should have access to the object - groupClaim: String - - # Allowed when the 'allow' argument is 'owner'. - # Specifies the field of type String or [String] that contains owner(s) that can access the object. - ownerField: String # defaults to "owner" - # Allowed when the 'allow' argument is 'groups'. - # Specifies the field of type String or [String] that contains group(s) that can access the object. - groupsField: String - - # Allowed when the 'allow' argument is 'groups'. - # Specifies a static list of groups that should have access to the object. - groups: [String] - - # Specifies operations to which this auth rule should be applied. - operations: [ModelOperation] - - # Deprecated. It is recommended to use the 'operations' arguments. - queries: [ModelQuery] @deprecated(reason: "The 'queries' argument will be replaced by the 'operations' argument in a future release.") - - # Deprecated. It is recommended to use the 'operations' arguments. - mutations: [ModelMutation] - @deprecated(reason: "The 'mutations' argument will be replaced by the 'operations' argument in a future release.") - } - - enum AuthStrategy { - owner - groups - private - public - } - - enum AuthProvider { - apiKey - iam - oidc - userPools - } - - enum ModelOperation { - create - update - delete - read - } - - enum ModelQuery @deprecated(reason: "ModelQuery will be replaced by the 'ModelOperation' in a future release.") { - get - list - } - - enum ModelMutation @deprecated(reason: "ModelMutation will be replaced by the 'ModelOperation' in a future release.") { - create - update - delete - } - - directive @searchable(queries: SearchableQueryMap) on OBJECT - - input SearchableQueryMap { - search: String - } - - directive @deprecated(reason: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ENUM | ENUM_VALUE - - # GraphQL vNext Directives - # primaryKey directive - directive @primaryKey(sortKeyFields: [String!], queryField: String) on FIELD_DEFINITION - directive @index(name: String!, sortKeyFields: [String], queryField: String) on FIELD_DEFINITION - - directive @hasOne(fields: [String!]) on FIELD_DEFINITION - directive @hasMany(indexName: String, fields: [String], limit: Int = 100) on FIELD_DEFINITION - directive @belongsTo(fields: [String]) on FIELD_DEFINITION - directive @manyToMany(relationName: String!) on FIELD_DEFINITION -`; - -export const scalars = [ - 'ID', - 'String', - 'Int', - 'Float', - 'Boolean', - 'AWSDate', - 'AWSDateTime', - 'AWSTime', - 'AWSTimestamp', - 'AWSEmail', - 'AWSJSON', - 'AWSURL', - 'AWSPhone', - 'AWSIPAddress', -] - .map(typeName => `scalar ${typeName}`) - .join(); diff --git a/packages/appsync-modelgen-plugin/src/scalars/supported-scalars.ts b/packages/appsync-modelgen-plugin/src/scalars/supported-scalars.ts new file mode 100644 index 000000000..c047f55a2 --- /dev/null +++ b/packages/appsync-modelgen-plugin/src/scalars/supported-scalars.ts @@ -0,0 +1,19 @@ +// Used only in tests. Directive definition will be passed as part of the configuration when modelgen is run using CLI +export const scalars = [ + 'ID', + 'String', + 'Int', + 'Float', + 'Boolean', + 'AWSDate', + 'AWSDateTime', + 'AWSTime', + 'AWSTimestamp', + 'AWSEmail', + 'AWSJSON', + 'AWSURL', + 'AWSPhone', + 'AWSIPAddress', +] + .map(typeName => `scalar ${typeName}`) + .join(); diff --git a/packages/appsync-modelgen-plugin/src/schemas/introspection/1/ModelIntrospectionSchema.json b/packages/appsync-modelgen-plugin/src/schemas/introspection/1/ModelIntrospectionSchema.json index bc8428316..d92926be2 100644 --- a/packages/appsync-modelgen-plugin/src/schemas/introspection/1/ModelIntrospectionSchema.json +++ b/packages/appsync-modelgen-plugin/src/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/visitors/appsync-model-introspection-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-model-introspection-visitor.ts index e58672743..ff7ed1758 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,17 @@ 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'; import modelIntrospectionSchema from '../schemas/introspection/1/ModelIntrospectionSchema.json'; +type UnionFieldType = { union: string }; +type InterfaceFieldType = { interface: string }; + export interface RawAppSyncModelIntrospectionConfig extends RawAppSyncModelConfig {}; export interface ParsedAppSyncModelIntrospectionConfig extends ParsedAppSyncModelConfig {}; export class AppSyncModelIntrospectionVisitor< @@ -63,23 +67,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; } @@ -119,10 +147,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: [], }; @@ -150,6 +184,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 @@ -170,7 +228,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) { @@ -182,7 +240,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; @@ -196,7 +254,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 { @@ -211,4 +278,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'; + } } diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-visitor.ts index 34c2de3f5..603ff91ca 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; + } } diff --git a/packages/graphql-generator/API.md b/packages/graphql-generator/API.md index 2d8864923..d514d95c0 100644 --- a/packages/graphql-generator/API.md +++ b/packages/graphql-generator/API.md @@ -20,7 +20,7 @@ export function generateModels(options: GenerateModelsOptions): Promise { + value: GraphQLResult; +} + +export type Blog = { + __typename: \\"Blog\\"; + id: string; + name: string; + posts?: ModelPostConnection | null; + createdAt: string; + updatedAt: string; +}; + +export type ModelPostConnection = { + __typename: \\"ModelPostConnection\\"; + items: Array; + nextToken?: string | null; +}; + +export type Post = { + __typename: \\"Post\\"; + id: string; + title: string; + blog?: Blog | null; + comments?: ModelCommentConnection | null; + createdAt: string; + updatedAt: string; + blogPostsId?: string | null; +}; + +export type ModelCommentConnection = { + __typename: \\"ModelCommentConnection\\"; + items: Array; + nextToken?: string | null; +}; + +export type Comment = { + __typename: \\"Comment\\"; + id: string; + post?: Post | null; + content: string; + createdAt: string; + updatedAt: string; + postCommentsId?: string | null; +}; + +export type GetBlogQuery = { + __typename: \\"Blog\\"; + id: string; + name: string; + posts?: { + __typename: \\"ModelPostConnection\\"; + nextToken?: string | null; + } | null; + createdAt: string; + updatedAt: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + async GetBlog(id: string): Promise { + const statement = \`query GetBlog($id: ID!) { + getBlog(id: $id) { + __typename + id + name + posts { + __typename + nextToken + } + createdAt + updatedAt + } + }\`; + const gqlAPIServiceArguments: any = { + id + }; + const response = (await API.graphql( + graphqlOperation(statement, gqlAPIServiceArguments) + )) as any; + return response.data.getBlog; + } +} +", +} +`; + +exports[`generateTypes amplifyJsLibraryVersion generates angular types for v6 when value is 6 1`] = ` +Object { + "api.service.ts": "/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. +import { Injectable } from \\"@angular/core\\"; +import { Client, generateClient, GraphQLResult } from \\"aws-amplify/api\\"; +import { Observable } from \\"rxjs\\"; + +export type Blog = { + __typename: \\"Blog\\"; + id: string; + name: string; + posts?: ModelPostConnection | null; + createdAt: string; + updatedAt: string; +}; + +export type ModelPostConnection = { + __typename: \\"ModelPostConnection\\"; + items: Array; + nextToken?: string | null; +}; + +export type Post = { + __typename: \\"Post\\"; + id: string; + title: string; + blog?: Blog | null; + comments?: ModelCommentConnection | null; + createdAt: string; + updatedAt: string; + blogPostsId?: string | null; +}; + +export type ModelCommentConnection = { + __typename: \\"ModelCommentConnection\\"; + items: Array; + nextToken?: string | null; +}; + +export type Comment = { + __typename: \\"Comment\\"; + id: string; + post?: Post | null; + content: string; + createdAt: string; + updatedAt: string; + postCommentsId?: string | null; +}; + +export type GetBlogQuery = { + __typename: \\"Blog\\"; + id: string; + name: string; + posts?: { + __typename: \\"ModelPostConnection\\"; + nextToken?: string | null; + } | null; + createdAt: string; + updatedAt: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + public client: Client; + constructor() { + this.client = generateClient(); + } + async GetBlog(id: string): Promise { + const statement = \`query GetBlog($id: ID!) { + getBlog(id: $id) { + __typename + id + name + posts { + __typename + nextToken + } + createdAt + updatedAt + } + }\`; + const gqlAPIServiceArguments: any = { + id + }; + const response = (await this.client.graphql({ + query: statement, + variables: gqlAPIServiceArguments + })) as any; + return response.data.getBlog; + } +} +", +} +`; + exports[`generateTypes multipleSwiftFiles generates multiple files 1`] = ` Object { "Types.graphql.swift": "// This file was automatically generated and should not be edited. diff --git a/packages/graphql-generator/src/__tests__/models.test.ts b/packages/graphql-generator/src/__tests__/models.test.ts index b3055680f..c089e8166 100644 --- a/packages/graphql-generator/src/__tests__/models.test.ts +++ b/packages/graphql-generator/src/__tests__/models.test.ts @@ -9,7 +9,6 @@ describe('generateModels', () => { const options: GenerateModelsOptions = { schema: readSchema('blog-model.graphql'), target, - directives, }; const models = await generateModels(options); expect(models).toMatchSnapshot(); @@ -20,122 +19,27 @@ describe('generateModels', () => { const options: GenerateModelsOptions = { schema: readSchema('blog-model.graphql'), target: 'swift', - directives, improvePluralization: true, }; const models = await generateModels(options); expect(models).toMatchSnapshot(); }); }); -}); - -const directives = ` -directive @aws_subscribe(mutations: [String!]!) on FIELD_DEFINITION - -directive @aws_auth(cognito_groups: [String!]!) on FIELD_DEFINITION - -directive @aws_api_key on FIELD_DEFINITION | OBJECT - -directive @aws_iam on FIELD_DEFINITION | OBJECT - -directive @aws_oidc on FIELD_DEFINITION | OBJECT -directive @aws_cognito_user_pools(cognito_groups: [String!]) on FIELD_DEFINITION | OBJECT - -directive @aws_lambda on FIELD_DEFINITION | OBJECT - -directive @deprecated(reason: String) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ENUM | ENUM_VALUE - -directive @model(queries: ModelQueryMap, mutations: ModelMutationMap, subscriptions: ModelSubscriptionMap, timestamps: TimestampConfiguration) on OBJECT -input ModelMutationMap { - create: String - update: String - delete: String -} -input ModelQueryMap { - get: String - list: String -} -input ModelSubscriptionMap { - onCreate: [String] - onUpdate: [String] - onDelete: [String] - level: ModelSubscriptionLevel -} -enum ModelSubscriptionLevel { - off - public - on -} -input TimestampConfiguration { - createdAt: String - updatedAt: String -} -directive @function(name: String!, region: String, accountId: String) repeatable on FIELD_DEFINITION -directive @http(method: HttpMethod = GET, url: String!, headers: [HttpHeader] = []) on FIELD_DEFINITION -enum HttpMethod { - GET - POST - PUT - DELETE - PATCH -} -input HttpHeader { - key: String - value: String -} -directive @predictions(actions: [PredictionsActions!]!) on FIELD_DEFINITION -enum PredictionsActions { - identifyText - identifyLabels - convertTextToSpeech - translateText -} -directive @primaryKey(sortKeyFields: [String]) on FIELD_DEFINITION -directive @index(name: String, sortKeyFields: [String], queryField: String) repeatable on FIELD_DEFINITION -directive @hasMany(indexName: String, fields: [String!], limit: Int = 100) on FIELD_DEFINITION -directive @hasOne(fields: [String!]) on FIELD_DEFINITION -directive @manyToMany(relationName: String!, limit: Int = 100) on FIELD_DEFINITION -directive @belongsTo(fields: [String!]) on FIELD_DEFINITION -directive @default(value: String!) on FIELD_DEFINITION -directive @auth(rules: [AuthRule!]!) on OBJECT | FIELD_DEFINITION -input AuthRule { - allow: AuthStrategy! - provider: AuthProvider - identityClaim: String - groupClaim: String - ownerField: String - groupsField: String - groups: [String] - operations: [ModelOperation] -} -enum AuthStrategy { - owner - groups - private - public - custom -} -enum AuthProvider { - apiKey - iam - oidc - userPools - function -} -enum ModelOperation { - create - update - delete - read - list - get - sync - listen - search -} -directive @mapsTo(name: String!) on OBJECT -directive @searchable(queries: SearchableQueryMap) on OBJECT -input SearchableQueryMap { - search: String -}`; + test('does not fail on custom directives', async () => { + const options: GenerateModelsOptions = { + schema: ` + type Blog @customModel { + id: ID! + name: String! @customField + }`, + target: 'introspection', + directives: ` + directive @customModel on OBJECT + directive @customField on FIELD_DEFINITION + `, + }; + const models = await generateModels(options); + expect(models).toMatchSnapshot(); + }); +}); diff --git a/packages/graphql-generator/src/__tests__/types.test.ts b/packages/graphql-generator/src/__tests__/types.test.ts index 63ecf6f23..d019951d1 100644 --- a/packages/graphql-generator/src/__tests__/types.test.ts +++ b/packages/graphql-generator/src/__tests__/types.test.ts @@ -65,4 +65,29 @@ describe('generateTypes', () => { expect(generateTypes(options)).rejects.toThrow('Query documents must be of type Source[] when generating multiple Swift files.'); }); }); + + describe('amplifyJsLibraryVersion', () => { + test('generates angular types for v5 when value is 5 or undefined', async () => { + const options: GenerateTypesOptions = { + schema: sdlSchema, + queries, + target: 'angular', + amplifyJsLibraryVersion: 5, + }; + const typesV5 = await generateTypes(options); + const typesUndefined = await generateTypes({ ...options, amplifyJsLibraryVersion: undefined }); + expect(typesV5).toEqual(typesUndefined); + expect(typesV5).toMatchSnapshot(); + }); + test('generates angular types for v6 when value is 6', async () => { + const options: GenerateTypesOptions = { + schema: sdlSchema, + queries, + target: 'angular', + amplifyJsLibraryVersion: 6, + }; + const types = await generateTypes(options); + expect(types).toMatchSnapshot(); + }); + }); }); diff --git a/packages/graphql-generator/src/models.ts b/packages/graphql-generator/src/models.ts index 8723bb54c..c563c3778 100644 --- a/packages/graphql-generator/src/models.ts +++ b/packages/graphql-generator/src/models.ts @@ -1,15 +1,18 @@ import * as path from 'path'; import { parse } from 'graphql'; import * as appSyncDataStoreCodeGen from '@aws-amplify/appsync-modelgen-plugin'; +import { DefaultDirectives } from '@aws-amplify/graphql-directives'; import { codegen } from '@graphql-codegen/core'; import { ModelsTarget, GenerateModelsOptions, GeneratedOutput } from './typescript'; const { version: packageVersion } = require('../package.json'); +const directiveDefinitions = DefaultDirectives.map(directive => directive.definition).join('\n'); + export async function generateModels(options: GenerateModelsOptions): Promise { const { schema, target, - directives, + directives = directiveDefinitions, isDataStoreEnabled, // feature flags diff --git a/packages/graphql-generator/src/types.ts b/packages/graphql-generator/src/types.ts index bbae55687..85126a81c 100644 --- a/packages/graphql-generator/src/types.ts +++ b/packages/graphql-generator/src/types.ts @@ -3,11 +3,12 @@ import { generate, generateFromString } from '@aws-amplify/graphql-types-generat import { GenerateTypesOptions, GeneratedOutput } from './typescript'; export async function generateTypes(options: GenerateTypesOptions): Promise { - const { schema, target, queries, multipleSwiftFiles = false, introspection = false } = options; + const { schema, target, queries, multipleSwiftFiles = false, introspection = false, amplifyJsLibraryVersion } = options; const generatedOutput = await generateFromString(schema, introspection, queries, target, multipleSwiftFiles, { addTypename: true, complexObjectSupport: 'auto', + amplifyJsLibraryVersion, }); return Object.fromEntries(Object.entries(generatedOutput).map(([filepath, contents]) => [path.basename(filepath), contents])); diff --git a/packages/graphql-generator/src/typescript.ts b/packages/graphql-generator/src/typescript.ts index da8228201..9be264370 100644 --- a/packages/graphql-generator/src/typescript.ts +++ b/packages/graphql-generator/src/typescript.ts @@ -14,12 +14,13 @@ export type GenerateTypesOptions = { queries: string | Source[]; introspection?: boolean; multipleSwiftFiles?: boolean; // only used when target is swift + amplifyJsLibraryVersion?: number; // only used when target is angular }; export type GenerateModelsOptions = { schema: string; target: ModelsTarget; - directives: string; + directives?: string; isDataStoreEnabled?: boolean; // feature flags diff --git a/packages/graphql-types-generator/CHANGELOG.md b/packages/graphql-types-generator/CHANGELOG.md index 647379e9d..abfecde87 100644 --- a/packages/graphql-types-generator/CHANGELOG.md +++ b/packages/graphql-types-generator/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [3.5.0](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-types-generator@3.4.6...@aws-amplify/graphql-types-generator@3.5.0) (2024-04-03) + +### Features + +- add angular codegen v6 support ([#799](https://github.com/aws-amplify/amplify-codegen/issues/799)) ([7d1a269](https://github.com/aws-amplify/amplify-codegen/commit/7d1a26941547a26640f7dc4aa25da9c0e1dab654)) + ## [3.4.6](https://github.com/aws-amplify/amplify-codegen/compare/@aws-amplify/graphql-types-generator@3.4.5...@aws-amplify/graphql-types-generator@3.4.6) (2023-12-11) **Note:** Version bump only for package @aws-amplify/graphql-types-generator diff --git a/packages/graphql-types-generator/package.json b/packages/graphql-types-generator/package.json index 248944eee..4bc6a1343 100644 --- a/packages/graphql-types-generator/package.json +++ b/packages/graphql-types-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/graphql-types-generator", - "version": "3.4.6", + "version": "3.5.0", "description": "Generate API code or type annotations based on a GraphQL schema and statements", "repository": { "type": "git", diff --git a/packages/graphql-types-generator/src/angular/index.ts b/packages/graphql-types-generator/src/angular/index.ts index bed0abde8..ae0d9510c 100644 --- a/packages/graphql-types-generator/src/angular/index.ts +++ b/packages/graphql-types-generator/src/angular/index.ts @@ -16,7 +16,15 @@ import { Property, interfaceDeclaration } from '../typescript/language'; import { isList } from '../utilities/graphql'; import { propertyDeclarations } from '../flow/codeGeneration'; -export function generateSource(context: LegacyCompilerContext) { +export function generateSource(context: LegacyCompilerContext, options?: { isAngularV6: boolean }) { + const isAngularV6: boolean = options?.isAngularV6 ?? false; + const importApiStatement = isAngularV6 + ? `import { Client, generateClient, GraphQLResult } from 'aws-amplify/api';` + : `import API, { graphqlOperation, GraphQLResult } from '@aws-amplify/api-graphql';` + const importObservable = isAngularV6 + ? `import { Observable } from 'rxjs';` + : `import { Observable } from 'zen-observable-ts';`; + const generator = new CodeGenerator(context); generator.printOnNewline('/* tslint:disable */'); @@ -24,23 +32,30 @@ export function generateSource(context: LegacyCompilerContext) { generator.printOnNewline('// This file was automatically generated and should not be edited.'); generator.printOnNewline(`import { Injectable } from '@angular/core';`); - generator.printOnNewline(`import API, { graphqlOperation, GraphQLResult } from '@aws-amplify/api-graphql';`); + generator.printOnNewline(importApiStatement); - generator.printOnNewline(`import { Observable } from 'zen-observable-ts';`); + generator.printOnNewline(importObservable); generator.printNewline(); - generateTypes(generator, context); + generateTypes(generator, context, { isAngularV6 }); generator.printNewline(); - generateAngularService(generator, context); + generateAngularService(generator, context, { isAngularV6 }); return prettier.format(generator.output, { parser: 'typescript' }); } -function generateTypes(generator: CodeGenerator, context: LegacyCompilerContext) { +function generateTypes(generator: CodeGenerator, context: LegacyCompilerContext, options?: { isAngularV6: boolean }) { + const isAngularV6: boolean = options?.isAngularV6 ?? false; // if subscription operations exist create subscriptionResponse interface // https://github.com/aws-amplify/amplify-cli/issues/5284 if (context.schema.getSubscriptionType()) { - generateSubscriptionResponseWrapper(generator); + if (!isAngularV6) { + /** + * V6 does not need to generate the response wrapper. + * The access pattern for V5 is `event.value.data` and `event.data` for V6 + */ + generateSubscriptionResponseWrapper(generator); + } generateSubscriptionOperationTypes(generator, context); } context.typesUsed.forEach(type => typeDeclarationForGraphQLType(generator, type)); @@ -152,7 +167,8 @@ function getReturnTypeName(generator: CodeGenerator, op: LegacyOperation): Strin } } -function generateAngularService(generator: CodeGenerator, context: LegacyCompilerContext) { +function generateAngularService(generator: CodeGenerator, context: LegacyCompilerContext, options?: { isAngularV6: boolean }) { + const isAngularV6: boolean = options?.isAngularV6 ?? false; const operations = context.operations; generator.printOnNewline(`@Injectable({ providedIn: 'root' @@ -160,19 +176,33 @@ function generateAngularService(generator: CodeGenerator, context: LegacyCompile generator.printOnNewline(`export class APIService {`); generator.withIndent(() => { + if (isAngularV6) { + generator.printOnNewline('public client: Client;'); + generateServiceConstructor(generator); + } Object.values(operations).forEach((op: LegacyOperation) => { if (op.operationType === 'subscription') { - return generateSubscriptionOperation(generator, op); + return generateSubscriptionOperation(generator, op, { isAngularV6 }); } if (op.operationType === 'query' || op.operationType === 'mutation') { - return generateQueryOrMutationOperation(generator, op); + return generateQueryOrMutationOperation(generator, op, { isAngularV6 }); } }); generator.printOnNewline('}'); }); } -function generateSubscriptionOperation(generator: CodeGenerator, op: LegacyOperation) { +function generateServiceConstructor(generator: CodeGenerator) { + generator.printOnNewline(); + generator.print(`constructor() {`); + generator.withIndent(() => { + generator.print(`this.client = generateClient();`); + }); + generator.printOnNewline('}'); +} + +function generateSubscriptionOperation(generator: CodeGenerator, op: LegacyOperation, options?: { isAngularV6: boolean }) { + const isAngularV6: boolean = options?.isAngularV6 ?? false; const statement = formatTemplateString(generator, op.source); const { operationName } = op; const vars = variablesFromField(generator.context, op.variables); @@ -180,29 +210,47 @@ function generateSubscriptionOperation(generator: CodeGenerator, op: LegacyOpera generator.printNewline(); const subscriptionName = `${operationName}Listener`; if (!vars.length) { - generator.print( - `${subscriptionName}: Observable> = API.graphql(graphqlOperation(\n\`${statement}\`)) as Observable>`, - ); + if (isAngularV6) { + generator.print( + `${subscriptionName}(): Observable> { return this.client.graphql({ query: \n\`${statement}\` }) as any; }`, + ); + } else { + generator.print( + `${subscriptionName}: Observable> = API.graphql(graphqlOperation(\n\`${statement}\`)) as Observable>`, + ); + } } else { generator.print(`${subscriptionName}(`); variableDeclaration(generator, vars); - generator.print(`) : Observable> {`); + if (isAngularV6) { + generator.print(`) : Observable> {`); + } else { + generator.print(`) : Observable> {`); + } generator.withIndent(() => { generator.printNewlineIfNeeded(); generator.print(`const statement = \`${statement}\``); const params = ['statement']; variableAssignmentToInput(generator, vars); params.push('gqlAPIServiceArguments'); - generator.printOnNewline( - `return API.graphql(graphqlOperation(${params.join(', ')})) as Observable>;`, - ); + if (isAngularV6) { + generator.printOnNewline( + `return this.client.graphql({ query: statement, variables: gqlAPIServiceArguments }) as any;`, + ); + } else { + generator.printOnNewline( + `return API.graphql(graphqlOperation(${params.join(', ')})) as Observable>;`, + ); + } + generator.printOnNewline('}'); }); } generator.printNewline(); } -function generateQueryOrMutationOperation(generator: CodeGenerator, op: LegacyOperation) { +function generateQueryOrMutationOperation(generator: CodeGenerator, op: LegacyOperation, options?: { isAngularV6: boolean }) { + const isAngularV6: boolean = options?.isAngularV6 ?? false; const statement = formatTemplateString(generator, op.source); const vars = variablesFromField(generator.context, op.variables); const returnType = getReturnTypeName(generator, op); @@ -221,7 +269,11 @@ function generateQueryOrMutationOperation(generator: CodeGenerator, op: LegacyOp variableAssignmentToInput(generator, vars); params.push('gqlAPIServiceArguments'); } - generator.printOnNewline(`const response = await API.graphql(graphqlOperation(${params.join(', ')})) as any;`); + if (isAngularV6) { + generator.printOnNewline(`const response = await this.client.graphql({ query: statement, ${op.variables.length ? `variables: gqlAPIServiceArguments, `: ''}}) as any;`); + } else { + generator.printOnNewline(`const response = await API.graphql(graphqlOperation(${params.join(', ')})) as any;`); + } generator.printOnNewline(`return (<${returnType}>response.data${resultProp})`); }); generator.printOnNewline('}'); diff --git a/packages/graphql-types-generator/src/generate.ts b/packages/graphql-types-generator/src/generate.ts index b60d8d589..e05de2f69 100644 --- a/packages/graphql-types-generator/src/generate.ts +++ b/packages/graphql-types-generator/src/generate.ts @@ -136,7 +136,7 @@ export function generateForTarget( case 'scala': return generateScalaSource(context, options); case 'angular': - return generateAngularSource(context); + return generateAngularSource(context, { isAngularV6: options.amplifyJsLibraryVersion === 6 }); default: throw new Error(`${target} is not supported.`); } diff --git a/packages/graphql-types-generator/src/utilities/getOutputFileName.ts b/packages/graphql-types-generator/src/utilities/getOutputFileName.ts index 408ac2f9d..a353eb770 100644 --- a/packages/graphql-types-generator/src/utilities/getOutputFileName.ts +++ b/packages/graphql-types-generator/src/utilities/getOutputFileName.ts @@ -23,7 +23,7 @@ const extensionMap: { 'typescript': string, 'flow': string, 'angular': string, ' swift: 'swift', }; -const folderMap: { 'typescript': string, 'flow': string, 'angular': string, 'swift': string } = { +const folderMap: { 'typescript': string, 'flow': string, 'angular': string, 'swift': string, } = { typescript: 'src', flow: 'src', angular: 'src/app', diff --git a/packages/graphql-types-generator/test/angular/__snapshots__/angularv6.ts.snap b/packages/graphql-types-generator/test/angular/__snapshots__/angularv6.ts.snap new file mode 100644 index 000000000..89aeb31f0 --- /dev/null +++ b/packages/graphql-types-generator/test/angular/__snapshots__/angularv6.ts.snap @@ -0,0 +1,400 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Angular code generation should generate simple query operations 1`] = ` +"/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. +import { Injectable } from \\"@angular/core\\"; +import { Client, generateClient, GraphQLResult } from \\"aws-amplify/api\\"; +import { Observable } from \\"rxjs\\"; + +export type Character = { + __typename: \\"Character\\"; + // The ID of the character + id: string; + // The name of the character + name: string; + // The friends of the character, or an empty list if they have none + friends?: Array | null; + // The friends of the character exposed as a connection with edges + friendsConnection: FriendsConnection; + // The movies this character appears in + appearsIn: Array; +}; + +export type Human = { + __typename: \\"Human\\"; + // The ID of the human + id: string; + // What this human calls themselves + name: string; + // The home planet of the human, or null if unknown + homePlanet?: string | null; + // Height in the preferred unit, default is meters + height?: number | null; + // Mass in kilograms, or null if unknown + mass?: number | null; + // This human's friends, or an empty list if they have none + friends?: Array | null; + // The friends of the human exposed as a connection with edges + friendsConnection: FriendsConnection; + // The movies this human appears in + appearsIn: Array; + // A list of starships this person has piloted, or an empty list if none + starships?: Array | null; +}; + +export type FriendsConnection = { + __typename: \\"FriendsConnection\\"; + // The total number of friends + totalCount?: number | null; + // The edges for each of the character's friends. + edges?: Array | null; + // A list of the friends, as a convenience when edges are not needed. + friends?: Array | null; + // Information for paginating this connection + pageInfo: PageInfo; +}; + +export type FriendsEdge = { + __typename: \\"FriendsEdge\\"; + // A cursor used for pagination + cursor: string; + // The character represented by this friendship edge + node?: Character | null; +}; + +export type PageInfo = { + __typename: \\"PageInfo\\"; + startCursor?: string | null; + endCursor?: string | null; + hasNextPage: boolean; +}; + +// The episodes in the Star Wars trilogy +export enum Episode { + NEWHOPE = \\"NEWHOPE\\", // Star Wars Episode IV: A New Hope, released in 1977. + EMPIRE = \\"EMPIRE\\", // Star Wars Episode V: The Empire Strikes Back, released in 1980. + JEDI = \\"JEDI\\" // Star Wars Episode VI: Return of the Jedi, released in 1983. +} + +export type Starship = { + __typename: \\"Starship\\"; + // The ID of the starship + id: string; + // The name of the starship + name: string; + // Length of the starship, along the longest axis + length?: number | null; + coordinates?: Array> | null; +}; + +export type Droid = { + __typename: \\"Droid\\"; + // The ID of the droid + id: string; + // What others call this droid + name: string; + // This droid's friends, or an empty list if they have none + friends?: Array | null; + // The friends of the droid exposed as a connection with edges + friendsConnection: FriendsConnection; + // The movies this droid appears in + appearsIn: Array; + // This droid's primary function + primaryFunction?: string | null; +}; + +export type HeroNameQuery = { + __typename: \\"Character\\"; + // The name of the character + name: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + public client: Client; + constructor() { + this.client = generateClient(); + } + async HeroName(): Promise { + const statement = \`query HeroName { + hero { + __typename + name + } + }\`; + const response = (await this.client.graphql({ query: statement })) as any; + return response.data.hero; + } +} +" +`; + +exports[`Angular code generation should generate simple query operations including input variables 1`] = ` +"/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. +import { Injectable } from \\"@angular/core\\"; +import { Client, generateClient, GraphQLResult } from \\"aws-amplify/api\\"; +import { Observable } from \\"rxjs\\"; + +// The episodes in the Star Wars trilogy +export enum Episode { + NEWHOPE = \\"NEWHOPE\\", // Star Wars Episode IV: A New Hope, released in 1977. + EMPIRE = \\"EMPIRE\\", // Star Wars Episode V: The Empire Strikes Back, released in 1980. + JEDI = \\"JEDI\\" // Star Wars Episode VI: Return of the Jedi, released in 1983. +} + +export type Character = { + __typename: \\"Character\\"; + // The ID of the character + id: string; + // The name of the character + name: string; + // The friends of the character, or an empty list if they have none + friends?: Array | null; + // The friends of the character exposed as a connection with edges + friendsConnection: FriendsConnection; + // The movies this character appears in + appearsIn: Array; +}; + +export type Human = { + __typename: \\"Human\\"; + // The ID of the human + id: string; + // What this human calls themselves + name: string; + // The home planet of the human, or null if unknown + homePlanet?: string | null; + // Height in the preferred unit, default is meters + height?: number | null; + // Mass in kilograms, or null if unknown + mass?: number | null; + // This human's friends, or an empty list if they have none + friends?: Array | null; + // The friends of the human exposed as a connection with edges + friendsConnection: FriendsConnection; + // The movies this human appears in + appearsIn: Array; + // A list of starships this person has piloted, or an empty list if none + starships?: Array | null; +}; + +export type FriendsConnection = { + __typename: \\"FriendsConnection\\"; + // The total number of friends + totalCount?: number | null; + // The edges for each of the character's friends. + edges?: Array | null; + // A list of the friends, as a convenience when edges are not needed. + friends?: Array | null; + // Information for paginating this connection + pageInfo: PageInfo; +}; + +export type FriendsEdge = { + __typename: \\"FriendsEdge\\"; + // A cursor used for pagination + cursor: string; + // The character represented by this friendship edge + node?: Character | null; +}; + +export type PageInfo = { + __typename: \\"PageInfo\\"; + startCursor?: string | null; + endCursor?: string | null; + hasNextPage: boolean; +}; + +export type Starship = { + __typename: \\"Starship\\"; + // The ID of the starship + id: string; + // The name of the starship + name: string; + // Length of the starship, along the longest axis + length?: number | null; + coordinates?: Array> | null; +}; + +export type Droid = { + __typename: \\"Droid\\"; + // The ID of the droid + id: string; + // What others call this droid + name: string; + // This droid's friends, or an empty list if they have none + friends?: Array | null; + // The friends of the droid exposed as a connection with edges + friendsConnection: FriendsConnection; + // The movies this droid appears in + appearsIn: Array; + // This droid's primary function + primaryFunction?: string | null; +}; + +export type HeroNameQuery = { + __typename: \\"Character\\"; + // The name of the character + name: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + public client: Client; + constructor() { + this.client = generateClient(); + } + async HeroName(episode?: Episode): Promise { + const statement = \`query HeroName($episode: Episode) { + hero(episode: $episode) { + __typename + name + } + }\`; + const gqlAPIServiceArguments: any = {}; + if (episode) { + gqlAPIServiceArguments.episode = episode; + } + const response = (await this.client.graphql({ + query: statement, + variables: gqlAPIServiceArguments + })) as any; + return response.data.hero; + } +} +" +`; + +exports[`Angular code generation should generate subscriptions 1`] = ` +"/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. +import { Injectable } from \\"@angular/core\\"; +import { Client, generateClient, GraphQLResult } from \\"aws-amplify/api\\"; +import { Observable } from \\"rxjs\\"; + +export type __SubscriptionContainer = { + onCreateRestaurant: OnCreateRestaurantSubscription; +}; + +export type Restaurant = { + __typename: \\"Restaurant\\"; + id: string; + name: string; + description: string; + city: string; +}; + +export type OnCreateRestaurantSubscription = { + __typename: \\"Restaurant\\"; + id: string; + name: string; + description: string; + city: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + public client: Client; + constructor() { + this.client = generateClient(); + } + OnCreateRestaurantListener(): Observable< + GraphQLResult> + > { + return this.client.graphql({ + query: \`subscription OnCreateRestaurant { + onCreateRestaurant { + __typename + id + name + description + city + } + }\` + }) as any; + } +} +" +`; + +exports[`Angular code generation should generate subscriptions with parameters 1`] = ` +"/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. +import { Injectable } from \\"@angular/core\\"; +import { Client, generateClient, GraphQLResult } from \\"aws-amplify/api\\"; +import { Observable } from \\"rxjs\\"; + +export type __SubscriptionContainer = { + onCreateRestaurant: OnCreateRestaurantSubscription; +}; + +export type Restaurant = { + __typename: \\"Restaurant\\"; + id: string; + name: string; + description: string; + city: string; + owner?: string | null; + createdAt: string; + updatedAt: string; +}; + +export type OnCreateRestaurantSubscription = { + __typename: \\"Restaurant\\"; + id: string; + name: string; + description: string; + city: string; + owner?: string | null; + createdAt: string; + updatedAt: string; +}; + +@Injectable({ + providedIn: \\"root\\" +}) +export class APIService { + public client: Client; + constructor() { + this.client = generateClient(); + } + OnCreateRestaurantListener( + owner: string + ): Observable< + GraphQLResult> + > { + const statement = \`subscription OnCreateRestaurant($owner: String!) { + onCreateRestaurant(owner: $owner) { + __typename + id + name + description + city + owner + createdAt + updatedAt + } + }\`; + const gqlAPIServiceArguments: any = { + owner + }; + return this.client.graphql({ + query: statement, + variables: gqlAPIServiceArguments + }) as any; + } +} +" +`; diff --git a/packages/graphql-types-generator/test/angular/angularv6.ts b/packages/graphql-types-generator/test/angular/angularv6.ts new file mode 100644 index 000000000..6d09e03d7 --- /dev/null +++ b/packages/graphql-types-generator/test/angular/angularv6.ts @@ -0,0 +1,108 @@ +import { CodeGenerator } from '../../src/utilities/CodeGenerator'; +import { compileToLegacyIR } from '../../src/compiler/legacyIR'; +import { parse } from 'graphql'; +import { generateSource } from '../../src/angular'; +import { loadSchema } from '../../src/loading'; + +const starWarsSchema = loadSchema(require.resolve('../fixtures/starwars/schema.json')); +const subscriptionSchema = loadSchema(require.resolve('../fixtures/misc/subscriptionSchema.json')); +const subscriptionSchemaWithParameters = require.resolve('../fixtures/misc/subscriptionSchemaWithParameters.graphql') + +describe('Angular code generation', () => { + let generator; + let compileFromSource; + let addFragment; + + const setup = (schema) => { + const context = { + schema: schema, + operations: {}, + fragments: {}, + typesUsed: {}, + }; + + generator = new CodeGenerator(context); + + compileFromSource = source => { + const document = parse(source); + const context = compileToLegacyIR(schema, document, { + mergeInFieldsFromFragmentSpreads: true, + addTypename: true, + }); + generator.context = context; + return context; + }; + + addFragment = fragment => { + generator.context.fragments[fragment.fragmentName] = fragment; + }; + + return { generator, compileFromSource, addFragment }; + } + + const generateAngularV6API = (context) => generateSource(context, { isAngularV6: true }) + + test(`should generate simple query operations`, function() { + const { compileFromSource } = setup(starWarsSchema); + const context = compileFromSource(` + query HeroName { + hero { + name + } + } + `); + + const source = generateAngularV6API(context); + expect(source).toMatchSnapshot(); + }); + + test(`should generate simple query operations including input variables`, function() { + const { compileFromSource } = setup(starWarsSchema); + const context = compileFromSource(` + query HeroName($episode: Episode) { + hero(episode: $episode) { + name + } + } + `); + + const source = generateAngularV6API(context); + expect(source).toMatchSnapshot(); + }); + + test(`should generate subscriptions`, function() { + const { compileFromSource } = setup(subscriptionSchema); + const context = compileFromSource(` + subscription OnCreateRestaurant { + onCreateRestaurant { + id + name + description + city + } + } + `); + + const source = generateAngularV6API(context); + expect(source).toMatchSnapshot(); + }); + + test('should generate subscriptions with parameters', () => { + const { compileFromSource } = setup(loadSchema(subscriptionSchemaWithParameters)); + const context = compileFromSource(` + subscription OnCreateRestaurant($owner: String!) { + onCreateRestaurant(owner: $owner) { + id + name + description + city + owner + createdAt + updatedAt + } + } + `); + const source = generateAngularV6API(context); + expect(source).toMatchSnapshot(); + }) +}) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 546cd360f..7173fd0e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -223,6 +223,11 @@ camelcase-keys "6.2.2" tslib "^1.8.0" +"@aws-amplify/graphql-directives@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@aws-amplify/graphql-directives/-/graphql-directives-1.0.1.tgz#8e78a7e6c4d936e80a7d1f6f2c9665d0b7b1552c" + integrity sha512-oUUQJU1syzUfU4P4z+fLDLklU9wmEZokgQOAKHBN5Szest/mDkjZEbG9VVBiMaQbq1Rw0jkRJmVU9WA1vxjT2A== + "@aws-amplify/graphql-schema-test-library@^1.1.18": version "1.1.27" resolved "https://registry.npmjs.org/@aws-amplify/graphql-schema-test-library/-/graphql-schema-test-library-1.1.27.tgz#30f5a732ce65031169907c386777f0bc0adfa0c2"