From bcb889ff00d4b09edbbab9908f233a4f850d9289 Mon Sep 17 00:00:00 2001 From: lazpavel <85319655+lazpavel@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:19:26 -0400 Subject: [PATCH 1/9] Add .circleci/config.yml From 091d882fd6387cf0e1b774d2283df17ee0255382 Mon Sep 17 00:00:00 2001 From: Pavel Lazar Date: Thu, 29 Jul 2021 22:48:14 -0400 Subject: [PATCH 2/9] test: ported @model v1 test suite to v2 --- jest.config.js | 1 + .../package.json | 6 +- .../model-directive-arguments.test.ts | 251 ++++++++++++ .../src/__tests__/model-transformer.test.ts | 381 ++++++++++++++++++ .../src/__tests__/test-utils/helpers.ts | 67 +++ 5 files changed, 704 insertions(+), 2 deletions(-) create mode 100644 packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts create mode 100644 packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts create mode 100644 packages/amplify-graphql-model-transformer/src/__tests__/test-utils/helpers.ts diff --git a/jest.config.js b/jest.config.js index 515f9b8cce8..e8b0bc6d65b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -47,6 +47,7 @@ module.exports = { '/packages/amplify-graphql-function-transformer', '/packages/amplify-graphql-http-transformer', '/packages/amplify-graphql-index-transformer', + '/packages/amplify-graphql-model-transformer', '/packages/amplify-graphql-predictions-transformer', '/packages/amplify-graphql-searchable-transformer', '/packages/amplify-graphql-types-generator', diff --git a/packages/amplify-graphql-model-transformer/package.json b/packages/amplify-graphql-model-transformer/package.json index 3563cdddd69..37f03ed6e0c 100644 --- a/packages/amplify-graphql-model-transformer/package.json +++ b/packages/amplify-graphql-model-transformer/package.json @@ -23,7 +23,9 @@ "scripts": { "build": "tsc", "watch": "tsc -w", - "clean": "rimraf ./lib" + "clean": "rimraf ./lib", + "test": "jest", + "test-watch": "jest --watch" }, "dependencies": { "@aws-amplify/graphql-transformer-core": "0.8.0", @@ -67,7 +69,7 @@ "^.+\\.tsx?$": "ts-jest" }, "testURL": "http://localhost", - "testRegex": "(src/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "testRegex": "(src/__tests__/.*\\.(test|spec))\\.(jsx?|tsx?)$", "moduleFileExtensions": [ "ts", "tsx", diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts new file mode 100644 index 00000000000..bec8e94881b --- /dev/null +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts @@ -0,0 +1,251 @@ +import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; +import { GraphQLTransform } from '@aws-amplify/graphql-transformer-core'; +import { parse } from 'graphql'; +import { getFieldOnObjectType, getObjectType } from './test-utils/helpers'; + +describe('createdAt field tests', () => { + it('should return createdAt when there is no timestamps configuration', () => { + const doc = /* GraphQL */ ` + type Post @model { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); + + expect(postModelField).toBeDefined(); + }); + + it('should return null when timestamps are set to null', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: null) { + id: ID! + title: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); + + expect(postModelField).toBeUndefined(); + }); + + it('should return null when createdAt is set to null', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: { createdAt: null }) { + id: ID! + title: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); + + expect(postModelField).toBeUndefined(); + }); + + it('should return createdOn when createdAt is set to createdOn', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: { createdAt: "createdOn" }) { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'createdOn'); + + expect(postModelField).toBeDefined(); + }); + + it('should return createdAt when createdAt is not set in timestamps', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: { updatedAt: "updatedOn" }) { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); + + expect(postModelField).toBeDefined(); + }); +}); + +describe('updatedAt field tests', () => { + it('should return updatedAt when there is no timestamps configuration', () => { + const doc = /* GraphQL */ ` + type Post @model { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); + + expect(postModelField).toBeDefined(); + }); + + it('should return null for updatedAt when timestamps are set to null', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: null) { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); + + expect(postModelField).toBeUndefined(); + }); + + it('should return null when updatedAt is set to null', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: { updatedAt: null }) { + id: ID! + title: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); + + expect(postModelField).toBeUndefined(); + }); + + it('should return updatedOn when updatedAt is set to updatedOn', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: { updatedAt: "updatedOn" }) { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'updatedOn'); + + expect(postModelField).toBeDefined(); + }); + + it('should return updatedAt when updatedAt is not set in timestamps', () => { + const doc = /* GraphQL */ ` + type Post @model(timestamps: { createdAt: "createdOnOn" }) { + id: ID! + title: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + }); + const out = transformer.transform(doc); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const postModelObject = getObjectType(parsed, 'Post'); + const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); + + expect(postModelField).toBeDefined(); + }); +}); diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts new file mode 100644 index 00000000000..c50e9ad456f --- /dev/null +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -0,0 +1,381 @@ +import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; +import { GraphQLTransform } from '@aws-amplify/graphql-transformer-core'; +import { FeatureFlagProvider } from '@aws-amplify/graphql-transformer-interfaces'; +import { InputObjectTypeDefinitionNode, InputValueDefinitionNode, NamedTypeNode, parse } from 'graphql'; +import { getBaseType } from 'graphql-transformer-common'; +import { + expectFields, + expectFieldsOnInputType, + getFieldOnInputType, + getFieldOnObjectType, + getInputType, + getObjectType, + verifyMatchingTypes, +} from './test-utils/helpers'; + +const featureFlags = { + getBoolean: jest.fn().mockImplementation((name, defaultValue) => { + if (name === 'improvePluralization') { + return true; + } + if (name === 'validateTypeNameReservedWords') { + return false; + } + return; + }), + getNumber: jest.fn(), + getObject: jest.fn(), + getString: jest.fn(), +}; + +describe('ModelTransformer: ', () => { + it('should successfully transform simple valid schema', async () => { + const validSchema = ` + type Post @model { + id: ID! + title: String! + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + }); + + it('should support custom query overrides', () => { + const validSchema = `type Post @model(queries: { get: "customGetPost", list: "customListPost" }) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + + const definition = out.schema; + expect(definition).toBeDefined(); + + const parsed = parse(definition); + const createPostInput = getInputType(parsed, 'CreatePostInput'); + expect(createPostInput).toBeDefined(); + + expectFieldsOnInputType(createPostInput!, ['id', 'title', 'createdAt', 'updatedAt']); + + // This id should always be optional. + // aka a named type node aka name.value would not be set if it were a non null node + const idField = createPostInput!.fields!.find(f => f.name.value === 'id'); + expect((idField!.type as NamedTypeNode).name!.value).toEqual('ID'); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).toBeDefined(); + expectFields(queryType!, ['customGetPost']); + expectFields(queryType!, ['customListPost']); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeDefined(); + expectFields(subscriptionType!, ['onCreatePost', 'onUpdatePost', 'onDeletePost']); + const subField = subscriptionType!.fields!.find(f => f.name.value === 'onCreatePost'); + expect(subField).toBeDefined(); + expect(subField!.directives!.length).toEqual(1); + expect(subField!.directives![0].name!.value).toEqual('aws_subscribe'); + }); + + it('should support custom mutations overrides', () => { + const validSchema = `type Post @model(mutations: { create: "customCreatePost", update: "customUpdatePost", delete: "customDeletePost" }) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + `; + + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsedDefinition = parse(definition); + const mutationType = getObjectType(parsedDefinition, 'Mutation'); + expect(mutationType).toBeDefined(); + expectFields(mutationType!, ['customCreatePost', 'customUpdatePost', 'customDeletePost']); + }); + + it('should not generate mutations when mutations are set to null', () => { + const validSchema = `type Post @model(mutations: null) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).not.toBeDefined(); + }); + + it('should not generate queries when queries are set to null', () => { + const validSchema = `type Post @model(queries: null) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).not.toBeDefined(); + }); + + it('should not generate subscriptions with subscriptions are set to null', () => { + const validSchema = `type Post @model(subscriptions: null) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).toBeDefined(); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).not.toBeDefined(); + }); + + it('should not generate subscriptions, mutations or queries when subscriptions, queries and mutations set to null', () => { + const validSchema = `type Post @model(queries: null, mutations: null, subscriptions: null) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).not.toBeDefined(); + const queryType = getObjectType(parsed, 'Query'); + expect(queryType).not.toBeDefined(); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).not.toBeDefined(); + }); + + it('should support mutation input overrides when mutations are disabled', () => { + const validSchema = `type Post @model(mutations: null) { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + input CreatePostInput { + different: String + } + input UpdatePostInput { + different2: String + } + input DeletePostInput { + different3: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const createPostInput = getInputType(parsed, 'CreatePostInput'); + expectFieldsOnInputType(createPostInput!, ['different']); + const updatePostInput = getInputType(parsed, 'UpdatePostInput'); + expectFieldsOnInputType(updatePostInput!, ['different2']); + const deletePostInput = getInputType(parsed, 'DeletePostInput'); + expectFieldsOnInputType(deletePostInput!, ['different3']); + }); + + it('should support mutation input overrides when mutations are enabled', () => { + const validSchema = `type Post @model { + id: ID! + title: String! + createdAt: String + updatedAt: String + } + # User defined types always take precedence. + input CreatePostInput { + different: String + } + input UpdatePostInput { + different2: String + } + input DeletePostInput { + different3: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const createPostInput = getInputType(parsed, 'CreatePostInput'); + expectFieldsOnInputType(createPostInput!, ['different']); + const updatePostInput = getInputType(parsed, 'UpdatePostInput'); + expectFieldsOnInputType(updatePostInput!, ['different2']); + const deletePostInput = getInputType(parsed, 'DeletePostInput'); + expectFieldsOnInputType(deletePostInput!, ['different3']); + }); + + it('should support non model objects contain id as a type for fields', () => { + const validSchema = ` + type Post @model { + id: ID! + comments: [Comment] + } + type Comment { + id: String! + text: String! + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + const commentInput = getInputType(parsed, 'CommentInput'); + expectFieldsOnInputType(commentInput!, ['id', 'text']); + const commentObject = getObjectType(parsed, 'Comment'); + const commentInputObject = getInputType(parsed, 'CommentInput'); + const commentObjectIDField = getFieldOnObjectType(commentObject!, 'id'); + const commentInputIDField = getFieldOnInputType(commentInputObject!, 'id'); + verifyMatchingTypes(commentObjectIDField.type, commentInputIDField.type); + }); + + it('should add default primary key when not defined', () => { + const validSchema = ` + type Post @model{ + str: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const result = transformer.transform(validSchema); + expect(result).toBeDefined(); + expect(result.schema).toBeDefined(); + const schema = parse(result.schema); + + const createPostInput: InputObjectTypeDefinitionNode = schema.definitions.find( + d => d.kind === 'InputObjectTypeDefinition' && d.name.value === 'CreatePostInput', + )! as InputObjectTypeDefinitionNode; + expect(createPostInput).toBeDefined(); + const defaultIdField: InputValueDefinitionNode = createPostInput.fields!.find(f => f.name.value === 'id')!; + expect(defaultIdField).toBeDefined(); + expect(getBaseType(defaultIdField.type)).toEqual('ID'); + }); + + it('should compile schema successfully when subscription is missing from schema', () => { + const validSchema = ` + type Post @model { + id: Int + str: String + } + + type Query { + Custom: String + } + + schema { + query: Query + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const parsed = parse(out.schema); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeDefined(); + expectFields(subscriptionType!, ['onCreatePost', 'onUpdatePost', 'onDeletePost']); + const mutationType = getObjectType(parsed, 'Mutation'); + expect(mutationType).toBeDefined(); + expectFields(mutationType!, ['createPost', 'updatePost', 'deletePost']); + }); + + it('should not validate reserved type names when validateTypeNameReservedWords is off', () => { + const schema = ` + type Subscription @model{ + id: Int + str: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags: ({ + getBoolean: jest.fn().mockImplementation(name => (name === 'validateTypeNameReservedWords' ? false : undefined)), + } as unknown) as FeatureFlagProvider, + }); + const out = transformer.transform(schema); + expect(out).toBeDefined(); + const parsed = parse(out.schema); + const subscriptionType = getObjectType(parsed, 'Subscription'); + expect(subscriptionType).toBeDefined(); + }); +}); diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/test-utils/helpers.ts b/packages/amplify-graphql-model-transformer/src/__tests__/test-utils/helpers.ts new file mode 100644 index 00000000000..16c22fdf1a7 --- /dev/null +++ b/packages/amplify-graphql-model-transformer/src/__tests__/test-utils/helpers.ts @@ -0,0 +1,67 @@ +import { + DefinitionNode, + DocumentNode, + FieldDefinitionNode, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + Kind, + ObjectTypeDefinitionNode, + parse, + TypeNode, +} from 'graphql'; + +export function getInputType(doc: DocumentNode, type: string): InputObjectTypeDefinitionNode | undefined { + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type) as + | InputObjectTypeDefinitionNode + | undefined; +} + +export function expectFieldsOnInputType(type: InputObjectTypeDefinitionNode, fields: string[]) { + for (const fieldName of fields) { + const foundField = type.fields!.find((f: InputValueDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } +} + +export function expectFields(type: ObjectTypeDefinitionNode, fields: string[]) { + for (const fieldName of fields) { + const foundField = type.fields!.find((f: FieldDefinitionNode) => f.name.value === fieldName); + expect(foundField).toBeDefined(); + } +} + +export function getObjectType(doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined { + return doc.definitions.find((def: DefinitionNode) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as + | ObjectTypeDefinitionNode + | undefined; +} + +export function doNotExpectFields(type: ObjectTypeDefinitionNode, fields: string[]) { + for (const fieldName of fields) { + expect(type.fields!.find((f: FieldDefinitionNode) => f.name.value === fieldName)).toBeUndefined(); + } +} + +export function verifyInputCount(doc: DocumentNode, type: string, count: number): boolean { + return doc.definitions.filter(def => def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION && def.name.value === type).length == count; +} + +export function getFieldOnInputType(type: InputObjectTypeDefinitionNode, field: string): InputValueDefinitionNode { + return type.fields!.find((f: InputValueDefinitionNode) => f.name.value === field)!; +} + +export function getFieldOnObjectType(type: ObjectTypeDefinitionNode, field: string): FieldDefinitionNode { + return type.fields!.find((f: FieldDefinitionNode) => f.name.value === field)!; +} + +export function verifyMatchingTypes(t1: TypeNode, t2: TypeNode): boolean { + if (t1.kind !== t2.kind) { + return false; + } + + if (t1.kind !== Kind.NAMED_TYPE && t2.kind !== Kind.NAMED_TYPE) { + return verifyMatchingTypes(t1.type, t2.type); + } else { + return false; + } +} From 29b9e0d1d2e78c868e9cb9e18057396bec5303aa Mon Sep 17 00:00:00 2001 From: Pavel Lazar Date: Fri, 30 Jul 2021 11:23:43 -0400 Subject: [PATCH 3/9] chore(graphql-model-transformer): removed improvePluralization check, V2 uses improvePluralization --- .../src/__tests__/model-transformer.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index c50e9ad456f..e2fcdde3eee 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -15,9 +15,6 @@ import { const featureFlags = { getBoolean: jest.fn().mockImplementation((name, defaultValue) => { - if (name === 'improvePluralization') { - return true; - } if (name === 'validateTypeNameReservedWords') { return false; } From d13db63f947318a5167404bb3bf716e4ef5b5269 Mon Sep 17 00:00:00 2001 From: Pavel Lazar Date: Fri, 30 Jul 2021 11:39:38 -0400 Subject: [PATCH 4/9] chore(graphql-model-transformer): added parse() to tests that generate a valid schema --- .../src/__tests__/model-transformer.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index e2fcdde3eee..618691b6e72 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -40,6 +40,8 @@ describe('ModelTransformer: ', () => { }); const out = transformer.transform(validSchema); expect(out).toBeDefined(); + + parse(out.schema); }); it('should support custom query overrides', () => { From 5a203106db8f64937a6f671a3a14e3510407b34c Mon Sep 17 00:00:00 2001 From: Pavel Lazar Date: Fri, 30 Jul 2021 15:10:47 -0400 Subject: [PATCH 5/9] test(graphql-model-transformer): added validateModelSchema check to existing model v2 tests --- .../model-directive-arguments.test.ts | 12 ++++- .../src/__tests__/model-transformer.test.ts | 44 ++++++------------- .../src/index.ts | 1 + 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts index bec8e94881b..bb7d73b36df 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-directive-arguments.test.ts @@ -1,5 +1,5 @@ import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; -import { GraphQLTransform } from '@aws-amplify/graphql-transformer-core'; +import { GraphQLTransform, validateModelSchema } from '@aws-amplify/graphql-transformer-core'; import { parse } from 'graphql'; import { getFieldOnObjectType, getObjectType } from './test-utils/helpers'; @@ -23,6 +23,7 @@ describe('createdAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); @@ -47,6 +48,7 @@ describe('createdAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); @@ -71,6 +73,7 @@ describe('createdAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); @@ -95,6 +98,7 @@ describe('createdAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'createdOn'); @@ -119,6 +123,7 @@ describe('createdAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'createdAt'); @@ -146,6 +151,7 @@ describe('updatedAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); @@ -171,6 +177,7 @@ describe('updatedAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); @@ -195,6 +202,7 @@ describe('updatedAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); @@ -219,6 +227,7 @@ describe('updatedAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'updatedOn'); @@ -243,6 +252,7 @@ describe('updatedAt field tests', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const postModelObject = getObjectType(parsed, 'Post'); const postModelField = getFieldOnObjectType(postModelObject!, 'updatedAt'); diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index 618691b6e72..7ea324e85de 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -1,5 +1,5 @@ import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; -import { GraphQLTransform } from '@aws-amplify/graphql-transformer-core'; +import { GraphQLTransform, validateModelSchema } from '@aws-amplify/graphql-transformer-core'; import { FeatureFlagProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { InputObjectTypeDefinitionNode, InputValueDefinitionNode, NamedTypeNode, parse } from 'graphql'; import { getBaseType } from 'graphql-transformer-common'; @@ -41,7 +41,7 @@ describe('ModelTransformer: ', () => { const out = transformer.transform(validSchema); expect(out).toBeDefined(); - parse(out.schema); + validateModelSchema(parse(out.schema)); }); it('should support custom query overrides', () => { @@ -65,6 +65,7 @@ describe('ModelTransformer: ', () => { expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const createPostInput = getInputType(parsed, 'CreatePostInput'); expect(createPostInput).toBeDefined(); @@ -106,6 +107,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsedDefinition = parse(definition); + validateModelSchema(parsedDefinition); const mutationType = getObjectType(parsedDefinition, 'Mutation'); expect(mutationType).toBeDefined(); expectFields(mutationType!, ['customCreatePost', 'customUpdatePost', 'customDeletePost']); @@ -128,6 +130,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const mutationType = getObjectType(parsed, 'Mutation'); expect(mutationType).not.toBeDefined(); }); @@ -150,6 +153,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const mutationType = getObjectType(parsed, 'Mutation'); expect(mutationType).toBeDefined(); const queryType = getObjectType(parsed, 'Query'); @@ -173,6 +177,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const mutationType = getObjectType(parsed, 'Mutation'); expect(mutationType).toBeDefined(); const queryType = getObjectType(parsed, 'Query'); @@ -198,6 +203,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const mutationType = getObjectType(parsed, 'Mutation'); expect(mutationType).not.toBeDefined(); const queryType = getObjectType(parsed, 'Query'); @@ -232,6 +238,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const createPostInput = getInputType(parsed, 'CreatePostInput'); expectFieldsOnInputType(createPostInput!, ['different']); const updatePostInput = getInputType(parsed, 'UpdatePostInput'); @@ -267,6 +274,7 @@ describe('ModelTransformer: ', () => { const definition = out.schema; expect(definition).toBeDefined(); const parsed = parse(definition); + validateModelSchema(parsed); const createPostInput = getInputType(parsed, 'CreatePostInput'); expectFieldsOnInputType(createPostInput!, ['different']); const updatePostInput = getInputType(parsed, 'UpdatePostInput'); @@ -275,35 +283,6 @@ describe('ModelTransformer: ', () => { expectFieldsOnInputType(deletePostInput!, ['different3']); }); - it('should support non model objects contain id as a type for fields', () => { - const validSchema = ` - type Post @model { - id: ID! - comments: [Comment] - } - type Comment { - id: String! - text: String! - } - `; - const transformer = new GraphQLTransform({ - transformers: [new ModelTransformer()], - featureFlags, - }); - const out = transformer.transform(validSchema); - expect(out).toBeDefined(); - const definition = out.schema; - expect(definition).toBeDefined(); - const parsed = parse(definition); - const commentInput = getInputType(parsed, 'CommentInput'); - expectFieldsOnInputType(commentInput!, ['id', 'text']); - const commentObject = getObjectType(parsed, 'Comment'); - const commentInputObject = getInputType(parsed, 'CommentInput'); - const commentObjectIDField = getFieldOnObjectType(commentObject!, 'id'); - const commentInputIDField = getFieldOnInputType(commentInputObject!, 'id'); - verifyMatchingTypes(commentObjectIDField.type, commentInputIDField.type); - }); - it('should add default primary key when not defined', () => { const validSchema = ` type Post @model{ @@ -318,6 +297,7 @@ describe('ModelTransformer: ', () => { expect(result).toBeDefined(); expect(result.schema).toBeDefined(); const schema = parse(result.schema); + validateModelSchema(schema); const createPostInput: InputObjectTypeDefinitionNode = schema.definitions.find( d => d.kind === 'InputObjectTypeDefinition' && d.name.value === 'CreatePostInput', @@ -350,6 +330,7 @@ describe('ModelTransformer: ', () => { const out = transformer.transform(validSchema); expect(out).toBeDefined(); const parsed = parse(out.schema); + validateModelSchema(parsed); const subscriptionType = getObjectType(parsed, 'Subscription'); expect(subscriptionType).toBeDefined(); expectFields(subscriptionType!, ['onCreatePost', 'onUpdatePost', 'onDeletePost']); @@ -374,6 +355,7 @@ describe('ModelTransformer: ', () => { const out = transformer.transform(schema); expect(out).toBeDefined(); const parsed = parse(out.schema); + validateModelSchema(parsed); const subscriptionType = getObjectType(parsed, 'Subscription'); expect(subscriptionType).toBeDefined(); }); diff --git a/packages/amplify-graphql-transformer-core/src/index.ts b/packages/amplify-graphql-transformer-core/src/index.ts index e3488cb0be9..d3f991998b2 100644 --- a/packages/amplify-graphql-transformer-core/src/index.ts +++ b/packages/amplify-graphql-transformer-core/src/index.ts @@ -14,6 +14,7 @@ export { UserPoolConfig, } from './transformation'; export { DeploymentResources } from './transformation/types'; +export { validateModelSchema } from './transformation/validation'; export { ConflictDetectionType, ConflictHandlerType, From a268c2d56b5bb5934cf12698ceacd92346b0ace4 Mon Sep 17 00:00:00 2001 From: Pavel Lazar Date: Mon, 2 Aug 2021 13:18:36 -0400 Subject: [PATCH 6/9] fix(graphql-model-transformer): fixed @model transform input object fields type Converted fields of input type objects to also be of input type. Small typo fixes. --- .../src/__tests__/model-transformer.test.ts | 30 +++++++++ .../src/graphql-model-transformer.ts | 8 +-- .../src/graphql-types/common.ts | 30 ++++----- .../src/graphql-types/mutation.ts | 17 ++--- .../src/graphql-types/query.ts | 4 +- .../src/wrappers/object-definition-wrapper.ts | 67 ++++++++++++------- ...aphql-searchable-transformer.tests.ts.snap | 4 +- 7 files changed, 106 insertions(+), 54 deletions(-) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index 7ea324e85de..c9a53f078d4 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -359,4 +359,34 @@ describe('ModelTransformer: ', () => { const subscriptionType = getObjectType(parsed, 'Subscription'); expect(subscriptionType).toBeDefined(); }); + + it('should support non model objects contain id as a type for fields', () => { + const validSchema = ` + type Post @model { + id: ID! + comments: [Comment] + } + type Comment { + id: String! + text: String! + } + `; + const transformer = new GraphQLTransform({ + transformers: [new ModelTransformer()], + featureFlags, + }); + const out = transformer.transform(validSchema); + expect(out).toBeDefined(); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); + validateModelSchema(parsed); + const commentInput = getInputType(parsed, 'CommentInput'); + expectFieldsOnInputType(commentInput!, ['id', 'text']); + const commentObject = getObjectType(parsed, 'Comment'); + const commentInputObject = getInputType(parsed, 'CommentInput'); + const commentObjectIDField = getFieldOnObjectType(commentObject!, 'id'); + const commentInputIDField = getFieldOnInputType(commentInputObject!, 'id'); + verifyMatchingTypes(commentObjectIDField.type, commentInputIDField.type); + }); }); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts index e73be42f1d1..339ce2f3b09 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts @@ -54,7 +54,7 @@ import { DirectiveWrapper, FieldWrapper, InputObjectDefinitionWrapper, - ObjectDefinationWrapper, + ObjectDefinitionWrapper, } from './wrappers/object-definition-wrapper'; export type Nullable = T | null; @@ -221,7 +221,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme if (mutation) { return mutation.fieldName; } - throw new Error('Unknow Subscription type'); + throw new Error('Unknown Subscription type'); }; const subscriptionsFields = this.getSubscriptionFieldNames(ctx, def!); @@ -796,7 +796,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme if (!typeObj) { throw new Error(`Type ${name} is missing in outputs`); } - const typeWrapper = new ObjectDefinationWrapper(typeObj); + const typeWrapper = new ObjectDefinitionWrapper(typeObj); if (!typeWrapper.hasField('id')) { const idField = FieldWrapper.create('id', 'ID'); typeWrapper.addField(idField); @@ -807,7 +807,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme if (typeWrapper.hasField(fieldName)) { const createdAtField = typeWrapper.getField(fieldName); if (!['String', 'AWSDateTime'].includes(createdAtField.getTypeName())) { - console.warn(`type ${name}.${fieldName} is not of String or AWSDateTime. Autopoupulation is not supported`); + console.warn(`type ${name}.${fieldName} is not of String or AWSDateTime. Auto population is not supported`); } } else { const createdAtField = FieldWrapper.create(fieldName, 'AWSDateTime'); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts b/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts index 499084e19f2..c1b3b910b58 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts @@ -31,9 +31,9 @@ import { } from '../definitions'; import { EnumWrapper, - InputFieldWraper, + InputFieldWrapper, InputObjectDefinitionWrapper, - ObjectDefinationWrapper, + ObjectDefinitionWrapper, } from '../wrappers/object-definition-wrapper'; /** @@ -48,7 +48,7 @@ export const makeConditionFilterInput = ( object: ObjectTypeDefinitionNode, ): InputObjectDefinitionWrapper => { const input = InputObjectDefinitionWrapper.create(name); - const wrappedObject = new ObjectDefinationWrapper(object); + const wrappedObject = new ObjectDefinitionWrapper(object); for (let field of wrappedObject.fields) { const fieldType = ctx.output.getType(field.getTypeName()); const isEnumType = fieldType && fieldType.kind === 'EnumTypeDefinition'; @@ -56,19 +56,19 @@ export const makeConditionFilterInput = ( const fieldTypeName = field.getTypeName(); const nameOverride = DEFAULT_SCALARS[fieldTypeName] || fieldTypeName; const conditionTypeName = isEnumType && field.isList() ? `Model${nameOverride}ListInput` : `Model${nameOverride}Input`; - const inputField = InputFieldWraper.create(field.name, conditionTypeName, true); + const inputField = InputFieldWrapper.create(field.name, conditionTypeName, true); input.addField(inputField); } } // additional conditions of list type for (let additionalField of ['and', 'or']) { - const inputField = InputFieldWraper.create(additionalField, name, true, true); + const inputField = InputFieldWrapper.create(additionalField, name, true, true); input.addField(inputField); } // additional conditions of non-list type for (let additionalField of ['not']) { - const inputField = InputFieldWraper.create(additionalField, name, true, false); + const inputField = InputFieldWrapper.create(additionalField, name, true, false); input.addField(inputField); } return input; @@ -105,7 +105,7 @@ export const createEnumModelFilters = ( type: ObjectTypeDefinitionNode, ): InputObjectTypeDefinitionNode[] => { // add enum type if present - const typeWrapper = new ObjectDefinationWrapper(type); + const typeWrapper = new ObjectDefinitionWrapper(type); const enumFields = typeWrapper.fields.filter(field => { const typeName = field.getTypeName(); const typeObj = ctx.output.getType(typeName); @@ -133,7 +133,7 @@ export function makeModelScalarFilterInputObject(type: string, supportsCondition default: typeName = type; } - const field = InputFieldWraper.create(condition, typeName, true); + const field = InputFieldWrapper.create(condition, typeName, true); if (condition === 'between') { field.wrapListType(); } @@ -177,20 +177,20 @@ function getFunctionListForType(typeName: string): Set { } } -function makeFunctionInputFields(typeName: string): InputFieldWraper[] { +function makeFunctionInputFields(typeName: string): InputFieldWrapper[] { const functions = getFunctionListForType(typeName); - const fields = new Array(); + const fields = new Array(); if (functions.has('attributeExists')) { - fields.push(InputFieldWraper.create('attributeExists', 'Boolean', true)); + fields.push(InputFieldWrapper.create('attributeExists', 'Boolean', true)); } if (functions.has('attributeType')) { - fields.push(InputFieldWraper.create('attributeType', 'ModelAttributeTypes', true)); + fields.push(InputFieldWrapper.create('attributeType', 'ModelAttributeTypes', true)); } if (functions.has('size')) { - fields.push(InputFieldWraper.create('size', 'ModelSizeInput', true)); + fields.push(InputFieldWrapper.create('size', 'ModelSizeInput', true)); } return fields; @@ -211,7 +211,7 @@ export function makeSizeInputType(): InputObjectTypeDefinitionNode { const input = InputObjectDefinitionWrapper.create(name); for (let condition of SIZE_CONDITIONS) { - const field = InputFieldWraper.create(condition, 'Int', true); + const field = InputFieldWrapper.create(condition, 'Int', true); if (condition === 'between') field.wrapListType(); input.addField(field); } @@ -222,7 +222,7 @@ export function makeEnumFilterInput(name: string): InputObjectTypeDefinitionNode const inputName = toPascalCase(['Model', name, 'Input']); const input = InputObjectDefinitionWrapper.create(inputName); ['eq', 'ne'].forEach(fieldName => { - const field = InputFieldWraper.create(fieldName, name, true); + const field = InputFieldWrapper.create(fieldName, name, true); input.addField(field); }); return input.serialize(); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts b/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts index e2ec3d7c475..04a4a780214 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts @@ -1,8 +1,8 @@ import { TransformerTransformSchemaStepContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { ObjectTypeDefinitionNode, InputObjectTypeDefinitionNode } from 'graphql'; -import { toPascalCase } from 'graphql-transformer-common'; +import { ModelResourceIDs, toPascalCase } from 'graphql-transformer-common'; import { ModelDirectiveConfiguration } from '../graphql-model-transformer'; -import { ObjectDefinationWrapper, InputObjectDefinitionWrapper, InputFieldWraper } from '../wrappers/object-definition-wrapper'; +import { ObjectDefinitionWrapper, InputObjectDefinitionWrapper, InputFieldWrapper } from '../wrappers/object-definition-wrapper'; import { makeConditionFilterInput } from './common'; /** @@ -17,7 +17,7 @@ export const makeUpdateInputField = ( knownModelTypes: Set, ): InputObjectTypeDefinitionNode => { // sync related things - const objectWrapped = new ObjectDefinationWrapper(obj); + const objectWrapped = new ObjectDefinitionWrapper(obj); const typeName = objectWrapped.name; const name = toPascalCase([`Update`, typeName, 'Input']); const hasIdField = objectWrapped.hasField('id'); @@ -42,7 +42,7 @@ export const makeUpdateInputField = ( // Add id field and make it optional if (!hasIdField) { - input.addField(InputFieldWraper.create('id', 'ID', false)); + input.addField(InputFieldWrapper.create('id', 'ID', false)); } else { const idField = input.fields.find(f => f.name === 'id'); if (idField) { @@ -69,7 +69,7 @@ export const makeUpdateInputField = ( export const makeDeleteInputField = (type: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode => { const name = toPascalCase(['Delete', type.name.value, 'input']); const inputField = InputObjectDefinitionWrapper.create(name); - const idField = InputFieldWraper.create('id', 'ID', false, false); + const idField = InputFieldWrapper.create('id', 'ID', false, false); inputField.addField(idField); return inputField.serialize(); }; @@ -86,9 +86,10 @@ export const makeCreateInputField = ( knownModelTypes: Set, ): InputObjectTypeDefinitionNode => { // sync related things - const objectWrapped = new ObjectDefinationWrapper(obj); + const objectWrapped = new ObjectDefinitionWrapper(obj); const typeName = objectWrapped.name; - const name = toPascalCase([`Create`, typeName, 'Input']); + const name = ModelResourceIDs.ModelCreateInputObjectName(typeName); + const hasIdField = objectWrapped.hasField('id'); const fieldsToRemove = objectWrapped .fields!.filter(field => { @@ -108,7 +109,7 @@ export const makeCreateInputField = ( // Add id field and make it optional if (!hasIdField) { - input.addField(InputFieldWraper.create('id', 'ID')); + input.addField(InputFieldWrapper.create('id', 'ID')); } else { const idField = input.fields.find(f => f.name === 'id'); if (idField) { diff --git a/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts b/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts index 767053e58de..dfe6c7ee1ce 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts @@ -1,6 +1,6 @@ import { TransformerTransformSchemaStepContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { InputObjectTypeDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; -import { FieldWrapper, ObjectDefinationWrapper } from '../wrappers/object-definition-wrapper'; +import { FieldWrapper, ObjectDefinitionWrapper } from '../wrappers/object-definition-wrapper'; import { makeConditionFilterInput } from './common'; export const makeListQueryFilterInput = ( ctx: TransformerTransformSchemaStepContextProvider, @@ -11,7 +11,7 @@ export const makeListQueryFilterInput = ( }; export const makeListQueryModel = (type: ObjectTypeDefinitionNode, modelName: string): ObjectTypeDefinitionNode => { - const outputType = ObjectDefinationWrapper.create(modelName); + const outputType = ObjectDefinitionWrapper.create(modelName); outputType.addField(FieldWrapper.create('items', type.name.value, true, true)); outputType.addField(FieldWrapper.create('nextToken', 'String', true, false)); diff --git a/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts b/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts index e2ad533af5d..b8077e85ee6 100644 --- a/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts +++ b/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts @@ -16,8 +16,9 @@ import { InputObjectTypeDefinitionNode, NamedTypeNode, EnumTypeDefinitionNode, + Kind, } from 'graphql'; -import { DEFAULT_SCALARS } from 'graphql-transformer-common'; +import { DEFAULT_SCALARS, getBaseType, isScalar, ModelResourceIDs, unwrapNonNull, withNamedNodeNamed } from 'graphql-transformer-common'; import { merge } from 'lodash'; // Todo: to be moved to core later. context.output.getObject would return wrapper type so its easier to manipulate @@ -121,7 +122,7 @@ export class GenericFieldWrapper { }; public getBaseType = (): NamedTypeNode => { let node = this.type; - while (node.kind === 'ListType' || node.kind === 'NonNullType') { + while (node.kind === Kind.LIST_TYPE || node.kind === Kind.NON_NULL_TYPE) { node = node.type; } return node; @@ -156,7 +157,7 @@ export class GenericFieldWrapper { // }; // } -export class InputFieldWraper extends GenericFieldWrapper { +export class InputFieldWrapper extends GenericFieldWrapper { public readonly argumenets?: InputValueDefinitionNode[]; public readonly description?: StringValueNode; public type: TypeNode; @@ -179,15 +180,39 @@ export class InputFieldWraper extends GenericFieldWrapper { directives: this.directives?.map(d => d.serialize()), }; }; - static fromField = (name: string, field: FieldDefinitionNode): InputFieldWraper => { - return new InputFieldWraper({ + + static fromField = (name: string, field: FieldDefinitionNode): InputFieldWrapper => { + const autoGeneratableFieldsWithType: Record = { + id: ['ID'], + createdAt: ['AWSDateTime', 'String'], + updatedAt: ['AWSDateTime', 'String'], + }; + + let type: TypeNode; + + if ( + Object.keys(autoGeneratableFieldsWithType).indexOf(name) !== -1 && + autoGeneratableFieldsWithType[name].indexOf(unwrapNonNull(field.type).name.value) !== -1 + ) { + // ids are always optional. when provided the value is used. + // when not provided the value is not used. + type = unwrapNonNull(field.type); + } else { + type = + isScalar(field.type) || getBaseType(field.type) === Kind.ENUM_TYPE_DEFINITION + ? field.type + : withNamedNodeNamed(field.type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))); + } + + return new InputFieldWrapper({ kind: 'InputValueDefinition', name: { kind: 'Name', value: name }, - type: field.type, + type, }); }; - static create = (name: string, type: string, isNullable = false, isList = false): InputFieldWraper => { - const field = new InputFieldWraper({ + + static create = (name: string, type: string, isNullable = false, isList = false): InputFieldWrapper => { + const field = new InputFieldWrapper({ kind: 'InputValueDefinition', name: { kind: 'Name', @@ -259,7 +284,7 @@ export class FieldWrapper extends GenericFieldWrapper { }; } -export class ObjectDefinationWrapper { +export class ObjectDefinitionWrapper { public readonly directives?: DirectiveWrapper[]; public readonly fields: FieldWrapper[]; public readonly name: string; @@ -310,8 +335,8 @@ export class ObjectDefinationWrapper { this.fields.splice(index, 1); }; - static create = (name: string, fields: FieldDefinitionNode[] = [], directives: DirectiveNode[] = []): ObjectDefinationWrapper => { - return new ObjectDefinationWrapper({ + static create = (name: string, fields: FieldDefinitionNode[] = [], directives: DirectiveNode[] = []): ObjectDefinitionWrapper => { + return new ObjectDefinitionWrapper({ kind: 'ObjectTypeDefinition', name: { kind: 'Name', @@ -325,11 +350,11 @@ export class ObjectDefinationWrapper { export class InputObjectDefinitionWrapper { public readonly directives?: DirectiveWrapper[]; - public readonly fields: InputFieldWraper[]; + public readonly fields: InputFieldWrapper[]; public readonly name: string; constructor(private node: InputObjectTypeDefinitionNode) { this.directives = (node.directives || []).map(d => new DirectiveWrapper(d)); - this.fields = (node.fields || []).map(f => new InputFieldWraper(f)); + this.fields = (node.fields || []).map(f => new InputFieldWrapper(f)); this.name = node.name.value; } @@ -345,7 +370,7 @@ export class InputObjectDefinitionWrapper { return field ? true : false; }; - getField = (name: string): InputFieldWraper => { + getField = (name: string): InputFieldWrapper => { const field = this.fields.find(f => f.name === name); if (!field) { throw new Error(`Field ${name} missing in type ${this.name}`); @@ -353,14 +378,14 @@ export class InputObjectDefinitionWrapper { return field; }; - addField = (field: InputFieldWraper): void => { + addField = (field: InputFieldWrapper): void => { if (this.hasField(field.name)) { throw new Error(`type ${this.name} has already a field with name ${field.name}`); } this.fields.push(field); }; - removeField = (field: InputFieldWraper): void => { + removeField = (field: InputFieldWrapper): void => { if (this.hasField(field.name)) { throw new Error(`type ${this.name} does not have the field with name ${field.name}`); } @@ -384,7 +409,7 @@ export class InputObjectDefinitionWrapper { directives: directives, }); for (let field of fields) { - const fieldWrapper = new InputFieldWraper(field); + const fieldWrapper = new InputFieldWrapper(field); wrappedObj.addField(fieldWrapper); } return wrappedObj; @@ -397,14 +422,10 @@ export class InputObjectDefinitionWrapper { fields: [], directives: [], }; + const wrappedInput = new InputObjectDefinitionWrapper(inputObj); for (let f of def.fields || []) { - const wrappedField = new InputFieldWraper({ - kind: 'InputValueDefinition', - name: f.name, - type: f.type, - directives: [], - }); + const wrappedField = InputFieldWrapper.fromField(f.name.value, f); wrappedInput.fields.push(wrappedField); } return wrappedInput; diff --git a/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap b/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap index bc0ae1eb6e2..179a69b1aa4 100644 --- a/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap +++ b/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap @@ -233,7 +233,7 @@ input CreateEmployeeInput { id: ID firstName: String! lastName: String! - type: EmploymentType! + type: EmploymentTypeInput! } type Mutation { @@ -246,7 +246,7 @@ input UpdateEmployeeInput { id: ID! firstName: String lastName: String - type: EmploymentType + type: EmploymentTypeInput } input DeleteEmployeeInput { From 02e1f4cc69d736667b821b7023e8d7ae90bde88d Mon Sep 17 00:00:00 2001 From: lazpavel <85319655+lazpavel@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:19:26 -0400 Subject: [PATCH 7/9] Add .circleci/config.yml From edc68626da82d1025a8279755f29d5e42bda625d Mon Sep 17 00:00:00 2001 From: lazpavel <85319655+lazpavel@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:19:26 -0400 Subject: [PATCH 8/9] Add .circleci/config.yml From 837f9bbdeb2fe80e019ae95350f16683a2616f3a Mon Sep 17 00:00:00 2001 From: Pavel Lazar Date: Tue, 3 Aug 2021 13:42:08 -0400 Subject: [PATCH 9/9] chore(graphql-model-transformer): removed test for deprecated @model feature flag --- .../src/__tests__/model-transformer.test.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index c9a53f078d4..4b20ee9cdc6 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -339,27 +339,6 @@ describe('ModelTransformer: ', () => { expectFields(mutationType!, ['createPost', 'updatePost', 'deletePost']); }); - it('should not validate reserved type names when validateTypeNameReservedWords is off', () => { - const schema = ` - type Subscription @model{ - id: Int - str: String - } - `; - const transformer = new GraphQLTransform({ - transformers: [new ModelTransformer()], - featureFlags: ({ - getBoolean: jest.fn().mockImplementation(name => (name === 'validateTypeNameReservedWords' ? false : undefined)), - } as unknown) as FeatureFlagProvider, - }); - const out = transformer.transform(schema); - expect(out).toBeDefined(); - const parsed = parse(out.schema); - validateModelSchema(parsed); - const subscriptionType = getObjectType(parsed, 'Subscription'); - expect(subscriptionType).toBeDefined(); - }); - it('should support non model objects contain id as a type for fields', () => { const validSchema = ` type Post @model {