diff --git a/packages/amplify-codegen/src/utils/getRelativeTypesPath.js b/packages/amplify-codegen/src/utils/getRelativeTypesPath.js index 7d3f8cb5b..7d8a406bc 100644 --- a/packages/amplify-codegen/src/utils/getRelativeTypesPath.js +++ b/packages/amplify-codegen/src/utils/getRelativeTypesPath.js @@ -2,7 +2,11 @@ const path = require('path'); function getRelativeTypesPath(opsGenDirectory, generatedFileName) { if (generatedFileName) { - const relativePath = path.relative(opsGenDirectory, generatedFileName); + const relativePath = path + .relative(opsGenDirectory, generatedFileName) + // ensure posix path separators are used + .split(path.win32.sep) + .join(path.posix.sep); // generatedFileName is in same directory as opsGenDirectory // i.e. generatedFileName: src/graphql/API.ts, opsGenDirectory: src/graphql diff --git a/packages/amplify-codegen/tests/commands/mock-fs-setup.js b/packages/amplify-codegen/tests/commands/mock-fs-setup.js index b0f275e7c..34dd71bc3 100644 --- a/packages/amplify-codegen/tests/commands/mock-fs-setup.js +++ b/packages/amplify-codegen/tests/commands/mock-fs-setup.js @@ -7,14 +7,14 @@ const { getOutputFileName } = require('@aws-amplify/graphql-types-generator'); * Mocks existence of `schema.json` using mocks fs * Mocks existence of `.graphqlconfig.yml` by mocking return value for loadConfig utility */ -function setupMocks(mockFs, loadConfig, apiId, frontend, target) { +function setupMocks(mockFs, loadConfig, apiId, frontend, target, generatedFileNameOverride, extendMockFs) { mockFs.restore(); const docsFilePath = { javascript: 'src/graphql', android: 'app/src/main/graphql/com/amazonaws/amplify/generated/graphql', swift: 'graphql', }; - const generatedFileName = getOutputFileName('API', target); + const generatedFileName = generatedFileNameOverride || getOutputFileName('API', target); const schemaFilePath = 'schema.json'; const nodeModulesPrettier = path.resolve(path.join(__dirname, '../../../../node_modules/prettier')); const mockedFiles = { @@ -26,6 +26,7 @@ function setupMocks(mockFs, loadConfig, apiId, frontend, target) { lazy: true, }), [schemaFilePath]: mockFs.load(path.resolve(path.join(__dirname, './blog-introspection-schema.json'))), + ...extendMockFs, }; mockFs(mockedFiles); diff --git a/packages/amplify-codegen/tests/commands/types-mock-fs.test.js b/packages/amplify-codegen/tests/commands/types-mock-fs.test.js index a35a59317..2a03488aa 100644 --- a/packages/amplify-codegen/tests/commands/types-mock-fs.test.js +++ b/packages/amplify-codegen/tests/commands/types-mock-fs.test.js @@ -90,6 +90,22 @@ describe('command - types (mock fs)', () => { }); }); + it('should generate multiple swift files if generatedFileName is a dir', async () => { + const generatedFileName = 'api'; + setupMocks(mockFs, loadConfig, MOCK_API_ID, 'swift', 'swift', generatedFileName, { [generatedFileName]: {} }); + + await generateStatements(MOCK_CONTEXT, false); + await generateTypes(MOCK_CONTEXT, false); + + expect(fs.existsSync(generatedFileName)).toBeTruthy(); + expect(fs.readdirSync(generatedFileName)).toEqual([ + 'Types.graphql.swift', + 'mutations.graphql.swift', + 'queries.graphql.swift', + 'subscriptions.graphql.swift', + ]); + }); + it('should not generate types when target is javascript', async () => { const generatedFileName = setupMocks(mockFs, loadConfig, MOCK_API_ID, 'javascript', 'javascript'); diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-typescript-visitor.test.ts.snap b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-typescript-visitor.test.ts.snap new file mode 100644 index 000000000..ba58434eb --- /dev/null +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/__snapshots__/appsync-typescript-visitor.test.ts.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TypeScript visitor list enum 1`] = ` +"import { ModelInit, MutableModel, PersistentModelConstructor } from \\"@aws-amplify/datastore\\"; +import { initSchema } from \\"@aws-amplify/datastore\\"; + +import { schema } from \\"./schema\\"; + +export enum DayOfWeek { + MONDAY = \\"MONDAY\\", + TUESDAY = \\"TUESDAY\\", + WEDNESDAY = \\"WEDNESDAY\\", + THURSDAY = \\"THURSDAY\\", + FRIDAY = \\"FRIDAY\\", + SATURDAY = \\"SATURDAY\\", + SUNDAY = \\"SUNDAY\\" +} + + + +type EagerRecurrenceModel = { + readonly daysOfWeek: DayOfWeek[] | Array; +} + +type LazyRecurrenceModel = { + readonly daysOfWeek: DayOfWeek[] | Array; +} + +export declare type RecurrenceModel = LazyLoading extends LazyLoadingDisabled ? EagerRecurrenceModel : LazyRecurrenceModel + +export declare const RecurrenceModel: (new (init: ModelInit) => RecurrenceModel) + +const { Recurrence } = initSchema(schema) as { + Recurrence: PersistentModelConstructor; +}; + +export { + +};" +`; + +exports[`TypeScript visitor singular enum 1`] = ` +"import { ModelInit, MutableModel, PersistentModelConstructor } from \\"@aws-amplify/datastore\\"; +import { initSchema } from \\"@aws-amplify/datastore\\"; + +import { schema } from \\"./schema\\"; + +export enum Frequency { + YEARLY = \\"YEARLY\\", + WEEKLY = \\"WEEKLY\\" +} + + + +type EagerRecurrenceModel = { + readonly frequency: Frequency | keyof typeof Frequency; +} + +type LazyRecurrenceModel = { + readonly frequency: Frequency | keyof typeof Frequency; +} + +export declare type RecurrenceModel = LazyLoading extends LazyLoadingDisabled ? EagerRecurrenceModel : LazyRecurrenceModel + +export declare const RecurrenceModel: (new (init: ModelInit) => RecurrenceModel) + +const { Recurrence } = initSchema(schema) as { + Recurrence: PersistentModelConstructor; +}; + +export { + +};" +`; diff --git a/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts new file mode 100644 index 000000000..3b53acb0f --- /dev/null +++ b/packages/appsync-modelgen-plugin/src/__tests__/visitors/appsync-typescript-visitor.test.ts @@ -0,0 +1,57 @@ +import { buildSchema, GraphQLSchema, parse, visit } from 'graphql'; +import { validateTs } from '@graphql-codegen/testing'; +import { TYPESCRIPT_SCALAR_MAP } from '../../scalars'; +import { directives, scalars } from '../../scalars/supported-directives'; +import { AppSyncModelTypeScriptVisitor } from '../../visitors/appsync-typescript-visitor'; + +const buildSchemaWithDirectives = (schema: String): GraphQLSchema => { + return buildSchema([schema, directives, scalars].join('\n')); +}; +const getVisitor = (schema: string): AppSyncModelTypeScriptVisitor => { + const ast = parse(schema); + const builtSchema = buildSchemaWithDirectives(schema); + const visitor = new AppSyncModelTypeScriptVisitor( + builtSchema, + { directives, target: 'typescript', scalars: TYPESCRIPT_SCALAR_MAP, codegenVersion: '3.3.4' }, + {}, + ); + visit(ast, { leave: visitor }); + return visitor; +}; + +describe('TypeScript visitor', () => { + test('singular enum', () => { + const schema = /* GraphQL */ ` + enum Frequency { + YEARLY + WEEKLY + } + + type Recurrence { + frequency: Frequency! + } + `; + const visitor = getVisitor(schema); + expect(visitor.generate()).toMatchSnapshot(); + }); + + test('list enum', () => { + const schema = /* GraphQL */ ` + enum DayOfWeek { + MONDAY + TUESDAY + WEDNESDAY + THURSDAY + FRIDAY + SATURDAY + SUNDAY + } + + type Recurrence { + daysOfWeek: [DayOfWeek!]! + } + `; + const visitor = getVisitor(schema); + expect(visitor.generate()).toMatchSnapshot(); + }); +}); diff --git a/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts b/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts index de4e66d5c..25f642f6c 100644 --- a/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts +++ b/packages/appsync-modelgen-plugin/src/visitors/appsync-typescript-visitor.ts @@ -48,7 +48,7 @@ export class AppSyncModelTypeScriptVisitor< this.processDirectives( shouldUseModelNameFieldInHasManyAndBelongsTo, shouldImputeKeyForUniDirectionalHasMany, - shouldUseFieldsInAssociatedWithInHasOne + shouldUseFieldsInAssociatedWithInHasOne, ); const imports = this.generateImports(); const enumDeclarations = Object.values(this.enumMap) @@ -320,7 +320,9 @@ export class AppSyncModelTypeScriptVisitor< let nativeType = super.getNativeType(field); if (this.isEnumType(field)) { - nativeType = `${nativeType} | keyof typeof ${this.getEnumName(this.enumMap[typeName])}`; + const baseEnumType = `keyof typeof ${this.getEnumName(this.enumMap[typeName])}`; + const enumType = field.isList ? `Array<${baseEnumType}>` : baseEnumType; + nativeType = `${nativeType} | ${enumType}`; } nativeType = nativeType + nullableTypeUnion; diff --git a/packages/graphql-generator/src/__tests__/utils/GraphQLStatementsFormatter.test.ts b/packages/graphql-generator/src/__tests__/utils/GraphQLStatementsFormatter.test.ts index 2ff2eda76..a9a049b7d 100644 --- a/packages/graphql-generator/src/__tests__/utils/GraphQLStatementsFormatter.test.ts +++ b/packages/graphql-generator/src/__tests__/utils/GraphQLStatementsFormatter.test.ts @@ -26,11 +26,16 @@ describe('GraphQL statements Formatter', () => { expect(formattedOutput).toMatchSnapshot(); }); - it('Generates formatted output for TS frontend', () => { + it('Generates formatted output for TS frontend with posix path', () => { const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries', '../API.ts').format(statements); expect(formattedOutput).toMatchSnapshot(); }); + it('Generates formatted output for TS frontend with windows path', () => { + const formattedOutput = new GraphQLStatementsFormatter('typescript', 'queries', '..\\API.ts').format(statements); + expect(formattedOutput).toMatchSnapshot(); + }); + it('Generates formatted output for Flow frontend', () => { const formattedOutput = new GraphQLStatementsFormatter('flow').format(statements); expect(formattedOutput).toMatchSnapshot(); diff --git a/packages/graphql-generator/src/__tests__/utils/__snapshots__/GraphQLStatementsFormatter.test.ts.snap b/packages/graphql-generator/src/__tests__/utils/__snapshots__/GraphQLStatementsFormatter.test.ts.snap index 4873bdc95..72114f01c 100644 --- a/packages/graphql-generator/src/__tests__/utils/__snapshots__/GraphQLStatementsFormatter.test.ts.snap +++ b/packages/graphql-generator/src/__tests__/utils/__snapshots__/GraphQLStatementsFormatter.test.ts.snap @@ -62,7 +62,33 @@ export const getProject = /* GraphQL */ \` " `; -exports[`GraphQL statements Formatter Generates formatted output for TS frontend 1`] = ` +exports[`GraphQL statements Formatter Generates formatted output for TS frontend with posix path 1`] = ` +"/* tslint:disable */ +/* eslint-disable */ +// this is an auto generated file. This will be overwritten + +import * as APITypes from \\"../API\\"; +type GeneratedQuery = string & { + __generatedQueryInput: InputType; + __generatedQueryOutput: OutputType; +}; + +export const getProject = /* GraphQL */ \`query GetProject($id: ID!) { + getProject(id: $id) { + id + name + createdAt + updatedAt + } +} +\` as GeneratedQuery< + APITypes.GetProjectQueryVariables, + APITypes.GetProjectQuery +>; +" +`; + +exports[`GraphQL statements Formatter Generates formatted output for TS frontend with windows path 1`] = ` "/* tslint:disable */ /* eslint-disable */ // this is an auto generated file. This will be overwritten diff --git a/packages/graphql-generator/src/utils/GraphQLStatementsFormatter.ts b/packages/graphql-generator/src/utils/GraphQLStatementsFormatter.ts index 4b76a4e73..0958df584 100644 --- a/packages/graphql-generator/src/utils/GraphQLStatementsFormatter.ts +++ b/packages/graphql-generator/src/utils/GraphQLStatementsFormatter.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import prettier, { BuiltInParserName } from 'prettier'; import { interfaceNameFromOperation, @@ -34,7 +35,12 @@ export class GraphQLStatementsFormatter { }[operation]; this.lintOverrides = []; this.headerComments = []; - this.typesPath = typesPath ? typesPath.replace(/.ts/i, '') : null; + this.typesPath = typesPath + ? typesPath.replace(/.ts/i, '') + // ensure posix path separators are used + .split(path.win32.sep) + .join(path.posix.sep) + : null; this.includeTypeScriptTypes = !!(this.language === 'typescript' && this.opTypeName && this.typesPath); }