diff --git a/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts b/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts index bfee67b16c..37367c8c19 100644 --- a/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts +++ b/packages/amplify-category-api/src/graphql-transformer/transformer-options-v2.ts @@ -290,6 +290,7 @@ const generateTransformParameters = ( enableTransformerCfnOutputs: true, allowDestructiveGraphqlSchemaUpdates: false, replaceTableUponGsiUpdate: false, + allowGen1Patterns: false, }; }; diff --git a/packages/amplify-graphql-api-construct/.jsii b/packages/amplify-graphql-api-construct/.jsii index 5c8d8f539f..9643fa262d 100644 --- a/packages/amplify-graphql-api-construct/.jsii +++ b/packages/amplify-graphql-api-construct/.jsii @@ -3734,7 +3734,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 858 + "line": 892 }, "name": "AddFunctionProps", "properties": [ @@ -3747,7 +3747,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 862 + "line": 896 }, "name": "dataSource", "type": { @@ -3763,7 +3763,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 867 + "line": 901 }, "name": "name", "type": { @@ -3780,7 +3780,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 902 + "line": 936 }, "name": "code", "optional": true, @@ -3798,7 +3798,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 874 + "line": 908 }, "name": "description", "optional": true, @@ -3816,7 +3816,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 881 + "line": 915 }, "name": "requestMappingTemplate", "optional": true, @@ -3834,7 +3834,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 888 + "line": 922 }, "name": "responseMappingTemplate", "optional": true, @@ -3852,7 +3852,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 895 + "line": 929 }, "name": "runtime", "optional": true, @@ -4148,7 +4148,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 138 + "line": 139 }, "parameters": [ { @@ -4183,7 +4183,7 @@ "kind": "class", "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 84 + "line": 85 }, "methods": [ { @@ -4195,7 +4195,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 283 + "line": 290 }, "name": "addDynamoDbDataSource", "parameters": [ @@ -4244,7 +4244,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 295 + "line": 302 }, "name": "addElasticsearchDataSource", "parameters": [ @@ -4291,7 +4291,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 305 + "line": 312 }, "name": "addEventBridgeDataSource", "parameters": [ @@ -4338,7 +4338,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 387 + "line": 394 }, "name": "addFunction", "parameters": [ @@ -4373,7 +4373,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 316 + "line": 323 }, "name": "addHttpDataSource", "parameters": [ @@ -4421,7 +4421,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 327 + "line": 334 }, "name": "addLambdaDataSource", "parameters": [ @@ -4469,7 +4469,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 338 + "line": 345 }, "name": "addNoneDataSource", "parameters": [ @@ -4508,7 +4508,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 349 + "line": 356 }, "name": "addOpenSearchDataSource", "parameters": [ @@ -4556,7 +4556,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 362 + "line": 369 }, "name": "addRdsDataSource", "parameters": [ @@ -4623,7 +4623,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 378 + "line": 385 }, "name": "addResolver", "parameters": [ @@ -4664,7 +4664,7 @@ "immutable": true, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 119 + "line": 120 }, "name": "apiId", "type": { @@ -4679,7 +4679,7 @@ "immutable": true, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 99 + "line": 100 }, "name": "generatedFunctionSlots", "type": { @@ -4712,7 +4712,7 @@ "immutable": true, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 104 + "line": 105 }, "name": "graphqlUrl", "type": { @@ -4728,7 +4728,7 @@ "immutable": true, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 109 + "line": 110 }, "name": "realtimeUrl", "type": { @@ -4743,7 +4743,7 @@ "immutable": true, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 88 + "line": 89 }, "name": "resources", "type": { @@ -4759,7 +4759,7 @@ "immutable": true, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 114 + "line": 115 }, "name": "apiKey", "optional": true, @@ -4782,7 +4782,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 761 + "line": 795 }, "name": "AmplifyGraphqlApiCfnResources", "properties": [ @@ -4795,7 +4795,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 815 + "line": 849 }, "name": "additionalCfnResources", "type": { @@ -4816,7 +4816,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 800 + "line": 834 }, "name": "amplifyDynamoDbTables", "type": { @@ -4837,7 +4837,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 790 + "line": 824 }, "name": "cfnDataSources", "type": { @@ -4858,7 +4858,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 785 + "line": 819 }, "name": "cfnFunctionConfigurations", "type": { @@ -4879,7 +4879,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 810 + "line": 844 }, "name": "cfnFunctions", "type": { @@ -4900,7 +4900,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 765 + "line": 799 }, "name": "cfnGraphqlApi", "type": { @@ -4916,7 +4916,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 770 + "line": 804 }, "name": "cfnGraphqlSchema", "type": { @@ -4932,7 +4932,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 780 + "line": 814 }, "name": "cfnResolvers", "type": { @@ -4953,7 +4953,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 805 + "line": 839 }, "name": "cfnRoles", "type": { @@ -4974,7 +4974,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 795 + "line": 829 }, "name": "cfnTables", "type": { @@ -4995,7 +4995,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 775 + "line": 809 }, "name": "cfnApiKey", "optional": true, @@ -5018,7 +5018,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 678 + "line": 712 }, "name": "AmplifyGraphqlApiProps", "properties": [ @@ -5032,7 +5032,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 695 + "line": 729 }, "name": "authorizationModes", "type": { @@ -5049,7 +5049,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 683 + "line": 717 }, "name": "definition", "type": { @@ -5066,7 +5066,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 689 + "line": 723 }, "name": "apiName", "optional": true, @@ -5085,7 +5085,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 710 + "line": 744 }, "name": "conflictResolution", "optional": true, @@ -5103,7 +5103,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 754 + "line": 788 }, "name": "dataStoreConfiguration", "optional": true, @@ -5123,7 +5123,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 703 + "line": 737 }, "name": "functionNameMap", "optional": true, @@ -5146,7 +5146,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 725 + "line": 759 }, "name": "functionSlots", "optional": true, @@ -5181,7 +5181,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 748 + "line": 782 }, "name": "outputStorageStrategy", "optional": true, @@ -5198,7 +5198,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 737 + "line": 771 }, "name": "predictionsBucket", "optional": true, @@ -5216,7 +5216,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 719 + "line": 753 }, "name": "stackMappings", "optional": true, @@ -5242,7 +5242,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 732 + "line": 766 }, "name": "transformerPlugins", "optional": true, @@ -5264,7 +5264,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 743 + "line": 777 }, "name": "translationBehavior", "optional": true, @@ -5287,7 +5287,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 822 + "line": 856 }, "name": "AmplifyGraphqlApiResources", "properties": [ @@ -5300,7 +5300,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 846 + "line": 880 }, "name": "cfnResources", "type": { @@ -5316,7 +5316,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 841 + "line": 875 }, "name": "functions", "type": { @@ -5337,7 +5337,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 826 + "line": 860 }, "name": "graphqlApi", "type": { @@ -5353,7 +5353,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 851 + "line": 885 }, "name": "nestedStacks", "type": { @@ -5374,7 +5374,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 836 + "line": 870 }, "name": "roles", "type": { @@ -5395,7 +5395,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 831 + "line": 865 }, "name": "tables", "type": { @@ -6434,7 +6434,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 612 + "line": 646 }, "name": "IAmplifyGraphqlDefinition", "properties": [ @@ -6449,7 +6449,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 637 + "line": 671 }, "name": "dataSourceStrategies", "type": { @@ -6483,7 +6483,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 623 + "line": 657 }, "name": "functionSlots", "type": { @@ -6517,7 +6517,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 617 + "line": 651 }, "name": "schema", "type": { @@ -6534,7 +6534,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 643 + "line": 677 }, "name": "customSqlDataSourceStrategies", "optional": true, @@ -6558,7 +6558,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 631 + "line": 665 }, "name": "referencedLambdaFunctions", "optional": true, @@ -6584,7 +6584,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 649 + "line": 683 }, "name": "IBackendOutputEntry", "properties": [ @@ -6597,7 +6597,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 658 + "line": 692 }, "name": "payload", "type": { @@ -6618,7 +6618,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 653 + "line": 687 }, "name": "version", "type": { @@ -6638,7 +6638,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 664 + "line": 698 }, "methods": [ { @@ -6649,7 +6649,7 @@ }, "locationInModule": { "filename": "src/types.ts", - "line": 671 + "line": 705 }, "name": "addBackendOutputEntry", "parameters": [ @@ -6999,7 +6999,7 @@ "kind": "interface", "locationInModule": { "filename": "src/types.ts", - "line": 504 + "line": 521 }, "name": "PartialTranslationBehavior", "properties": [ @@ -7014,7 +7014,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 596 + "line": 613 }, "name": "allowDestructiveGraphqlSchemaUpdates", "optional": true, @@ -7032,7 +7032,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 516 + "line": 533 }, "name": "disableResolverDeduping", "optional": true, @@ -7054,7 +7054,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 561 + "line": 578 }, "name": "enableAutoIndexQueryNames", "optional": true, @@ -7073,7 +7073,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 576 + "line": 593 }, "name": "enableSearchNodeToNodeEncryption", "optional": true, @@ -7091,7 +7091,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 582 + "line": 599 }, "name": "enableTransformerCfnOutputs", "optional": true, @@ -7109,7 +7109,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 541 + "line": 558 }, "name": "populateOwnerFieldForStaticGroupAuth", "optional": true, @@ -7128,7 +7128,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 606 + "line": 623 }, "name": "replaceTableUponGsiUpdate", "optional": true, @@ -7146,7 +7146,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 567 + "line": 584 }, "name": "respectPrimaryKeyAttributesOnConnectionField", "optional": true, @@ -7164,7 +7164,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 522 + "line": 539 }, "name": "sandboxModeEnabled", "optional": true, @@ -7185,7 +7185,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 554 + "line": 571 }, "name": "secondaryKeyAsGSI", "optional": true, @@ -7206,7 +7206,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 509 + "line": 526 }, "name": "shouldDeepMergeDirectiveConfigDefaults", "optional": true, @@ -7224,7 +7224,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 535 + "line": 552 }, "name": "subscriptionsInheritPrimaryAuth", "optional": true, @@ -7243,7 +7243,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 548 + "line": 565 }, "name": "suppressApiKeyGeneration", "optional": true, @@ -7261,7 +7261,7 @@ "immutable": true, "locationInModule": { "filename": "src/types.ts", - "line": 529 + "line": 546 }, "name": "useSubUsernameForDefaultIdentityClaim", "optional": true, @@ -8649,5 +8649,5 @@ } }, "version": "1.11.1", - "fingerprint": "mbJgTnkfO7c/VCMzV45+X/nVSdHmWSb2kNaqX7rObhM=" + "fingerprint": "q/hBMvTTdGW+dq4Oc4gawAUouMRXVTg7+IDQCEhJK38=" } \ No newline at end of file diff --git a/packages/amplify-graphql-api-construct/API.md b/packages/amplify-graphql-api-construct/API.md index 65c2f02b70..c9bf8c1709 100644 --- a/packages/amplify-graphql-api-construct/API.md +++ b/packages/amplify-graphql-api-construct/API.md @@ -314,6 +314,8 @@ export interface OptimisticConflictResolutionStrategy extends ConflictResolution // @public export interface PartialTranslationBehavior { readonly allowDestructiveGraphqlSchemaUpdates?: boolean; + // @internal + readonly _allowGen1Patterns?: boolean; readonly disableResolverDeduping?: boolean; readonly enableAutoIndexQueryNames?: boolean; readonly enableSearchNodeToNodeEncryption?: boolean; @@ -438,6 +440,8 @@ export interface TimeToLiveSpecification { // @public export interface TranslationBehavior { readonly allowDestructiveGraphqlSchemaUpdates: boolean; + // @internal + readonly _allowGen1Patterns: boolean; readonly disableResolverDeduping: boolean; readonly enableAutoIndexQueryNames: boolean; // (undocumented) diff --git a/packages/amplify-graphql-api-construct/package.json b/packages/amplify-graphql-api-construct/package.json index a25bf0ec2d..afcbf9f50b 100644 --- a/packages/amplify-graphql-api-construct/package.json +++ b/packages/amplify-graphql-api-construct/package.json @@ -164,7 +164,7 @@ "global": { "branches": 90, "functions": 90, - "lines": 60 + "lines": 59 } }, "coverageReporters": [ diff --git a/packages/amplify-graphql-api-construct/src/__tests__/__functional__/disable-gen1-patterns.test.ts b/packages/amplify-graphql-api-construct/src/__tests__/__functional__/disable-gen1-patterns.test.ts new file mode 100644 index 0000000000..56e167be5e --- /dev/null +++ b/packages/amplify-graphql-api-construct/src/__tests__/__functional__/disable-gen1-patterns.test.ts @@ -0,0 +1,357 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { AmplifyGraphqlApi } from '../../amplify-graphql-api'; +import { AmplifyGraphqlDefinition } from '../../amplify-graphql-definition'; + +/** + * Utility to test if schema is valid when gen 1 patterns are disabled + * @param schema schema to test + * @param allowGen1Patterns if gen 1 patterns are allowed. + */ +const verifySchema = (schema: string, allowGen1Patterns: boolean): void => { + const stack = new cdk.Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(schema), + authorizationModes: { + apiKeyConfig: { expires: cdk.Duration.days(7) }, + }, + translationBehavior: { + _allowGen1Patterns: allowGen1Patterns, + }, + }); + Template.fromStack(stack); +}; + +describe('_allowGen1Patterns', () => { + test('defaults to allow', () => { + const schema = ` + type Post @model { + tags: [Tag] @manyToMany(relationName: "PostTags") + } + + type Tag @model { + posts: [Post] @manyToMany(relationName: "PostTags") + } + `; + const stack = new cdk.Stack(); + expect( + () => + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(schema), + authorizationModes: { + apiKeyConfig: { expires: cdk.Duration.days(7) }, + }, + }), + ).not.toThrow(); + }); + + describe('_allowGen1Patterns: true', () => { + test('allows @manyToMany', () => { + expect(() => + verifySchema( + ` + type Post @model { + tags: [Tag] @manyToMany(relationName: "PostTags") + } + + type Tag @model { + posts: [Post] @manyToMany(relationName: "PostTags") + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows @searchable', () => { + expect(() => + verifySchema( + ` + type Post @model @searchable { + title: String + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows @predictions', () => { + const schema = ` + type Query { + recognizeLabelsFromImage: [String] @predictions(actions: [identifyLabels]) + } + `; + const stack = new cdk.Stack(); + expect( + () => + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(schema), + authorizationModes: { + apiKeyConfig: { expires: cdk.Duration.days(7) }, + }, + translationBehavior: { + _allowGen1Patterns: true, + }, + predictionsBucket: new Bucket(stack, 'myfakebucket'), + }), + ).not.toThrow(); + }); + + test('allows fields on @belongsTo', () => { + expect(() => + verifySchema( + ` + type Post @model { + authorID: ID + author: Author @belongsTo(fields: ["authorID"]) + } + + type Author @model { + posts: [Post] @hasMany + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows fields on @hasMany', () => { + expect(() => + verifySchema( + ` + type Post @model { + author: Author @belongsTo + } + + type Author @model { + postID: ID + posts: [Post] @hasMany(fields: ["postID"]) + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows fields on @hasOne', () => { + expect(() => + verifySchema( + ` + type Profile @model { + author: Author @belongsTo + } + + type Author @model { + profileID: ID + profile: Profile @hasOne(fields: ["profileID"]) + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows required @belongsTo fields', () => { + expect(() => + verifySchema( + ` + type Post @model { + author: Author! @belongsTo + } + + type Author @model { + posts: [Post] @hasMany + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows required @hasMany fields', () => { + expect(() => + verifySchema( + ` + type Post @model { + author: Author @belongsTo + } + + type Author @model { + posts: [Post]! @hasMany + } + `, + true, + ), + ).not.toThrow(); + }); + + test('allows required @hasOne fields', () => { + expect(() => + verifySchema( + ` + type Profile @model { + author: Author @belongsTo + } + + type Author @model { + profile: Profile! @hasOne + } + `, + true, + ), + ).not.toThrow(); + }); + }); + + describe('_allowGen1Patterns: false', () => { + test('does not allow @manyToMany', () => { + expect(() => + verifySchema( + ` + type Post @model { + tags: [Tag] @manyToMany(relationName: "PostTags") + } + + type Tag @model { + posts: [Post] @manyToMany(relationName: "PostTags") + } + `, + false, + ), + ).toThrow('Unknown directive "@manyToMany".'); + }); + + test('does not allow @searchable', () => { + expect(() => + verifySchema( + ` + type Post @model @searchable { + title: String + } + `, + false, + ), + ).toThrow('Unknown directive "@searchable".'); + }); + + test('does not allow @predictions', () => { + expect(() => + verifySchema( + ` + type Query { + recognizeLabelsFromImage: [String] @predictions(actions: [identifyLabels]) + } + `, + false, + ), + ).toThrow('Unknown directive "@predictions".'); + }); + + test('does not allow fields on @belongsTo', () => { + expect(() => + verifySchema( + ` + type Post @model { + authorID: ID + author: Author @belongsTo(fields: ["authorID"]) + } + + type Author @model { + posts: [Post] @hasMany + } + `, + false, + ), + ).toThrow('fields argument on @belongsTo is disallowed. Modify Post.author to use references instead.'); + }); + + test('does not allow fields on @hasMany', () => { + expect(() => + verifySchema( + ` + type Post @model { + author: Author @belongsTo + } + + type Author @model { + postID: ID + posts: [Post] @hasMany(fields: ["postID"]) + } + `, + false, + ), + ).toThrow('fields argument on @hasMany is disallowed. Modify Author.posts to use references instead.'); + }); + + test('does not allow fields on @hasOne', () => { + expect(() => + verifySchema( + ` + type Profile @model { + author: Author @belongsTo + } + + type Author @model { + profileID: ID + profile: Profile @hasOne(fields: ["profileID"]) + } + `, + false, + ), + ).toThrow('fields argument on @hasOne is disallowed. Modify Author.profile to use references instead.'); + }); + + test('does not allow required @belongsTo fields', () => { + expect(() => + verifySchema( + ` + type Post @model { + author: Author! @belongsTo + } + + type Author @model { + posts: [Post] @hasMany + } + `, + false, + ), + ).toThrow('@belongsTo cannot be used on required fields. Modify Post.author to be optional.'); + }); + + test('does not allow required @hasMany fields', () => { + expect(() => + verifySchema( + ` + type Post @model { + author: Author @belongsTo + } + + type Author @model { + posts: [Post]! @hasMany + } + `, + false, + ), + ).toThrow('@hasMany cannot be used on required fields. Modify Author.posts to be optional.'); + }); + + test('does not allow required @hasOne fields', () => { + expect(() => + verifySchema( + ` + type Profile @model { + author: Author @belongsTo + } + + type Author @model { + profile: Profile! @hasOne + } + `, + false, + ), + ).toThrow('@hasOne cannot be used on required fields. Modify Author.profile to be optional.'); + }); + }); +}); diff --git a/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts b/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts index a5cc95aa48..343b878fda 100644 --- a/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts +++ b/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts @@ -3,6 +3,7 @@ import { Construct } from 'constructs'; import { ExecuteTransformConfig, executeTransform } from '@aws-amplify/graphql-transformer'; import { NestedStack, Stack } from 'aws-cdk-lib'; import { AttributionMetadataStorage, StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; +import { TransformParameters } from '@aws-amplify/graphql-transformer-interfaces'; import { graphqlOutputKey } from '@aws-amplify/backend-output-schemas'; import type { GraphqlOutput, AwsAppsyncAuthenticationType } from '@aws-amplify/backend-output-schemas'; import { @@ -184,6 +185,14 @@ export class AmplifyGraphqlApi extends Construct { const assetProvider = new AssetProvider(this); + const mergedTranslationBehavior = { + ...defaultTranslationBehavior, + ...(translationBehavior ?? {}), + }; + const transformParameters: TransformParameters = { + ...mergedTranslationBehavior, + allowGen1Patterns: mergedTranslationBehavior._allowGen1Patterns, + }; const executeTransformConfig: ExecuteTransformConfig = { scope: this, nestedStackProvider: { @@ -204,14 +213,12 @@ export class AmplifyGraphqlApi extends Construct { ...definition.referencedLambdaFunctions, ...functionNameMap, }, + allowGen1Patterns: transformParameters.allowGen1Patterns, }, authConfig, stackMapping: stackMappings ?? {}, resolverConfig: this.dataStoreConfiguration ? convertToResolverConfig(this.dataStoreConfiguration) : undefined, - transformParameters: { - ...defaultTranslationBehavior, - ...(translationBehavior ?? {}), - }, + transformParameters, // CDK construct uses a custom resource. We'll define this explicitly here to remind ourselves that this value is unused in the CDK // construct flow rdsLayerMapping: undefined, diff --git a/packages/amplify-graphql-api-construct/src/internal/default-parameters.ts b/packages/amplify-graphql-api-construct/src/internal/default-parameters.ts index 5b3b780219..fe2c7a3084 100644 --- a/packages/amplify-graphql-api-construct/src/internal/default-parameters.ts +++ b/packages/amplify-graphql-api-construct/src/internal/default-parameters.ts @@ -20,4 +20,5 @@ export const defaultTranslationBehavior: TranslationBehavior = { enableTransformerCfnOutputs: false, allowDestructiveGraphqlSchemaUpdates: false, replaceTableUponGsiUpdate: false, + _allowGen1Patterns: true, }; diff --git a/packages/amplify-graphql-api-construct/src/types.ts b/packages/amplify-graphql-api-construct/src/types.ts index d58b6d9f48..9f066889dd 100644 --- a/packages/amplify-graphql-api-construct/src/types.ts +++ b/packages/amplify-graphql-api-construct/src/types.ts @@ -496,6 +496,23 @@ export interface TranslationBehavior { * @experimental */ readonly replaceTableUponGsiUpdate: boolean; + + /** + * When disabled usage of Gen 1 patterns will result in an error thrown. + * + * Gen 1 Patterns that will be disabled when set to false: + * - Use of @manyToMany + * - Use of @searchable + * - Use of @predictions + * - Use of fields argument on @hasOne, @hasMany, and @belongsTo. + * - Use of @hasOne, @hasMany, and @belongsTo on required fields. + * + * @default true + * @internal + * Warning: Although this has `public` access, it is intended for internal use and should not be used directly. + * The behavior of this may change without warning. + */ + readonly _allowGen1Patterns: boolean; } /** @@ -604,6 +621,23 @@ export interface PartialTranslationBehavior { * @experimental */ readonly replaceTableUponGsiUpdate?: boolean; + + /** + * When disabled usage of Gen 1 patterns will result in an error thrown. + * + * Gen 1 Patterns that will be disabled when set to false: + * - Use of @manyToMany + * - Use of @searchable + * - Use of @predictions + * - Use of fields argument on @hasOne, @hasMany, and @belongsTo. + * - Use of @hasOne, @hasMany, and @belongsTo on required fields. + * + * @default true + * @internal + * Warning: Although this has `public` access, it is intended for internal use and should not be used directly. + * The behavior of this may change without warning. + */ + readonly _allowGen1Patterns?: boolean; } /** diff --git a/packages/amplify-graphql-relational-transformer/src/graphql-belongs-to-transformer.ts b/packages/amplify-graphql-relational-transformer/src/graphql-belongs-to-transformer.ts index c144c58f87..23f212cd22 100644 --- a/packages/amplify-graphql-relational-transformer/src/graphql-belongs-to-transformer.ts +++ b/packages/amplify-graphql-relational-transformer/src/graphql-belongs-to-transformer.ts @@ -24,6 +24,7 @@ import { InterfaceTypeDefinitionNode, NamedTypeNode, ObjectTypeDefinitionNode, + Kind, } from 'graphql'; import { getBaseType, isListType, isNonNullType, makeField, makeNamedType, makeNonNullType } from 'graphql-transformer-common'; import produce from 'immer'; @@ -158,6 +159,20 @@ export class BelongsToTransformer extends TransformerPluginBase { const validate = (config: BelongsToDirectiveConfiguration, ctx: TransformerContextProvider): void => { const { field, object } = config; + if (!ctx.transformParameters.allowGen1Patterns) { + const modelName = object.name.value; + const fieldName = field.name.value; + if (field.type.kind === Kind.NON_NULL_TYPE) { + throw new InvalidDirectiveError( + `@${BelongsToDirective.name} cannot be used on required fields. Modify ${modelName}.${fieldName} to be optional.`, + ); + } + if (config.fields) { + throw new InvalidDirectiveError( + `fields argument on @${BelongsToDirective.name} is disallowed. Modify ${modelName}.${fieldName} to use references instead.`, + ); + } + } let dbType: ModelDataSourceStrategyDbType; try { diff --git a/packages/amplify-graphql-relational-transformer/src/graphql-has-many-transformer.ts b/packages/amplify-graphql-relational-transformer/src/graphql-has-many-transformer.ts index bbf0ecc503..a659ac2376 100644 --- a/packages/amplify-graphql-relational-transformer/src/graphql-has-many-transformer.ts +++ b/packages/amplify-graphql-relational-transformer/src/graphql-has-many-transformer.ts @@ -25,6 +25,7 @@ import { NamedTypeNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, + Kind, } from 'graphql'; import { getBaseType, isListType, isNonNullType, makeField, makeNamedType, makeNonNullType } from 'graphql-transformer-common'; import produce from 'immer'; @@ -157,7 +158,21 @@ export class HasManyTransformer extends TransformerPluginBase { } const validate = (config: HasManyDirectiveConfiguration, ctx: TransformerContextProvider): void => { - const { field } = config; + const { field, object } = config; + if (!ctx.transformParameters.allowGen1Patterns) { + const modelName = object.name.value; + const fieldName = field.name.value; + if (field.type.kind === Kind.NON_NULL_TYPE) { + throw new InvalidDirectiveError( + `@${HasManyDirective.name} cannot be used on required fields. Modify ${modelName}.${fieldName} to be optional.`, + ); + } + if (config.fields) { + throw new InvalidDirectiveError( + `fields argument on @${HasManyDirective.name} is disallowed. Modify ${modelName}.${fieldName} to use references instead.`, + ); + } + } if (!isListType(field.type)) { throw new InvalidDirectiveError(`@${HasManyDirective.name} must be used with a list. Use @hasOne for non-list types.`); diff --git a/packages/amplify-graphql-relational-transformer/src/graphql-has-one-transformer.ts b/packages/amplify-graphql-relational-transformer/src/graphql-has-one-transformer.ts index a778825d59..c4b1bde544 100644 --- a/packages/amplify-graphql-relational-transformer/src/graphql-has-one-transformer.ts +++ b/packages/amplify-graphql-relational-transformer/src/graphql-has-one-transformer.ts @@ -24,6 +24,7 @@ import { InterfaceTypeDefinitionNode, NamedTypeNode, ObjectTypeDefinitionNode, + Kind, } from 'graphql'; import { getBaseType, @@ -185,7 +186,21 @@ export class HasOneTransformer extends TransformerPluginBase { } const validate = (config: HasOneDirectiveConfiguration, ctx: TransformerContextProvider): void => { - const { field } = config; + const { field, object } = config; + if (!ctx.transformParameters.allowGen1Patterns) { + const modelName = object.name.value; + const fieldName = field.name.value; + if (field.type.kind === Kind.NON_NULL_TYPE) { + throw new InvalidDirectiveError( + `@${HasOneDirective.name} cannot be used on required fields. Modify ${modelName}.${fieldName} to be optional.`, + ); + } + if (config.fields) { + throw new InvalidDirectiveError( + `fields argument on @${HasOneDirective.name} is disallowed. Modify ${modelName}.${fieldName} to use references instead.`, + ); + } + } let dbType: ModelDataSourceStrategyDbType; try { diff --git a/packages/amplify-graphql-transformer-core/src/transformer-context/transform-parameters.ts b/packages/amplify-graphql-transformer-core/src/transformer-context/transform-parameters.ts index a73e90539b..f40c7ee1fb 100644 --- a/packages/amplify-graphql-transformer-core/src/transformer-context/transform-parameters.ts +++ b/packages/amplify-graphql-transformer-core/src/transformer-context/transform-parameters.ts @@ -10,6 +10,7 @@ export const defaultTransformParameters: TransformParameters = { sandboxModeEnabled: false, allowDestructiveGraphqlSchemaUpdates: false, replaceTableUponGsiUpdate: false, + allowGen1Patterns: true, // Auth Params useSubUsernameForDefaultIdentityClaim: true, diff --git a/packages/amplify-graphql-transformer-interfaces/API.md b/packages/amplify-graphql-transformer-interfaces/API.md index e2c537d77f..97195a5c56 100644 --- a/packages/amplify-graphql-transformer-interfaces/API.md +++ b/packages/amplify-graphql-transformer-interfaces/API.md @@ -914,6 +914,7 @@ export type TransformParameters = { sandboxModeEnabled: boolean; allowDestructiveGraphqlSchemaUpdates: boolean; replaceTableUponGsiUpdate: boolean; + allowGen1Patterns: boolean; useSubUsernameForDefaultIdentityClaim: boolean; populateOwnerFieldForStaticGroupAuth: boolean; suppressApiKeyGeneration: boolean; diff --git a/packages/amplify-graphql-transformer-interfaces/src/transformer-context/transform-parameters.ts b/packages/amplify-graphql-transformer-interfaces/src/transformer-context/transform-parameters.ts index af1eaffcdf..6f82377918 100644 --- a/packages/amplify-graphql-transformer-interfaces/src/transformer-context/transform-parameters.ts +++ b/packages/amplify-graphql-transformer-interfaces/src/transformer-context/transform-parameters.ts @@ -14,6 +14,7 @@ export type TransformParameters = { sandboxModeEnabled: boolean; allowDestructiveGraphqlSchemaUpdates: boolean; replaceTableUponGsiUpdate: boolean; + allowGen1Patterns: boolean; // Auth Params useSubUsernameForDefaultIdentityClaim: boolean; diff --git a/packages/amplify-graphql-transformer/API.md b/packages/amplify-graphql-transformer/API.md index 01a514a4ef..d8e628ddcf 100644 --- a/packages/amplify-graphql-transformer/API.md +++ b/packages/amplify-graphql-transformer/API.md @@ -56,6 +56,7 @@ export type TransformerFactoryArgs = { storageConfig?: any; customTransformers?: TransformerPluginProvider[]; functionNameMap?: Record; + allowGen1Patterns?: boolean; }; // (No @packageDocumentation comment for this package) diff --git a/packages/amplify-graphql-transformer/src/__tests__/graphql-transformer.test.ts b/packages/amplify-graphql-transformer/src/__tests__/graphql-transformer.test.ts index 1829d714b3..9cd5e3c2b6 100644 --- a/packages/amplify-graphql-transformer/src/__tests__/graphql-transformer.test.ts +++ b/packages/amplify-graphql-transformer/src/__tests__/graphql-transformer.test.ts @@ -37,6 +37,14 @@ describe('constructTransformerChain', () => { it('succeeds on admin roles', () => { expect(constructTransformerChain().length).toEqual(numOfTransformers); }); + + it('allows gen 1 patterns by default', () => { + expect(constructTransformerChain().length).toEqual(numOfTransformers); + }); + + it('removes transformers not supported in gen 2', () => { + expect(constructTransformerChain({ allowGen1Patterns: false }).length).toEqual(numOfTransformers - 3); + }); }); const defaultTransformConfig: TransformConfig = { @@ -56,6 +64,7 @@ const defaultTransformConfig: TransformConfig = { enableTransformerCfnOutputs: true, allowDestructiveGraphqlSchemaUpdates: false, replaceTableUponGsiUpdate: false, + allowGen1Patterns: true, }, }; diff --git a/packages/amplify-graphql-transformer/src/graphql-transformer.ts b/packages/amplify-graphql-transformer/src/graphql-transformer.ts index 563b91efdd..930a01127c 100644 --- a/packages/amplify-graphql-transformer/src/graphql-transformer.ts +++ b/packages/amplify-graphql-transformer/src/graphql-transformer.ts @@ -42,6 +42,7 @@ export type TransformerFactoryArgs = { storageConfig?: any; customTransformers?: TransformerPluginProvider[]; functionNameMap?: Record; + allowGen1Patterns?: boolean; }; /** @@ -62,24 +63,26 @@ export const constructTransformerChain = (options?: TransformerFactoryArgs): Tra const indexTransformer = new IndexTransformer(); const hasOneTransformer = new HasOneTransformer(); + const allowGen1Patterns = options?.allowGen1Patterns === undefined ? true : options?.allowGen1Patterns; + // The default list of transformers should match DefaultDirectives in packages/amplify-graphql-directives/src/index.ts return [ modelTransformer, new FunctionTransformer(options?.functionNameMap), new HttpTransformer(), - new PredictionsTransformer(options?.storageConfig), + ...(allowGen1Patterns ? [new PredictionsTransformer(options?.storageConfig)] : []), new PrimaryKeyTransformer(), indexTransformer, new HasManyTransformer(), hasOneTransformer, - new ManyToManyTransformer(modelTransformer, indexTransformer, hasOneTransformer, authTransformer), + ...(allowGen1Patterns ? [new ManyToManyTransformer(modelTransformer, indexTransformer, hasOneTransformer, authTransformer)] : []), new BelongsToTransformer(), new DefaultValueTransformer(), authTransformer, new MapsToTransformer(), new SqlTransformer(), new RefersToTransformer(), - new SearchableModelTransformer(), + ...(allowGen1Patterns ? [new SearchableModelTransformer()] : []), ...(options?.customTransformers ?? []), ]; }; diff --git a/packages/amplify-util-mock/src/__e2e__/utils/index.ts b/packages/amplify-util-mock/src/__e2e__/utils/index.ts index 85c1b167bb..6d9c3709d3 100644 --- a/packages/amplify-util-mock/src/__e2e__/utils/index.ts +++ b/packages/amplify-util-mock/src/__e2e__/utils/index.ts @@ -68,6 +68,7 @@ export const defaultTransformParams: Pick