diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 320704ce..fbbe35ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,12 +2,12 @@ name: Release on: push jobs: test: - runs-on: ubuntu-16.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '12' + node-version: '14' - run: yarn - run: yarn test - run: yarn end-to-end-test diff --git a/scaffold/package.json b/scaffold/package.json index 84f692b6..a0f0826d 100644 --- a/scaffold/package.json +++ b/scaffold/package.json @@ -68,6 +68,6 @@ "testdouble-jest": "2.0.0", "ts-jest": "26.1.3", "ts-node": "8.10.2", - "typescript": "3.9.7" + "typescript": "4.7.4" } } diff --git a/src/generate/generate-module.ts b/src/generate/generate-module.ts index c3aef255..997c4306 100755 --- a/src/generate/generate-module.ts +++ b/src/generate/generate-module.ts @@ -15,6 +15,7 @@ import getScalars from './parse-graphql/getScalars'; import { saveRenderedTemplate } from './helpers/saveRenderedTemplate'; import { findProjectMainPath } from './helpers/findProjectMainPath'; import { execQuietly } from './helpers/execQuietly'; +import getUnions from './parse-graphql/getUnions'; const debug = configureDebug('generate-module'); @@ -247,6 +248,8 @@ export const executeGeneration = async (appPrefix = '~app', generatedPrefix = '~ const federatedEntities = getFederatedEntities(schemaString); const interfaces = getInterfaces(schemaString); + const unions = getUnions(schemaString); + // Leaving this for now // eslint-disable-next-line no-param-reassign schemaString = schemaString.replace(/extend type/g, 'type'); @@ -254,11 +257,11 @@ export const executeGeneration = async (appPrefix = '~app', generatedPrefix = '~ const schema = buildSchema(source, { assumeValidSDL: true }); shelljs.mkdir('-p', `${projectMainPath}/src/${graphqlFileRootPath}/types/`); - const createInterfaceType = (interfaceName: string) => { + const createResolveType = (resolverTypeName: string) => { const templateName = './templates/typeTypeResolvers.handlebars'; const capitalizedFieldName = capitalize('__resolveType'); const context = { - typeName: interfaceName, + typeName: resolverTypeName, fieldName: '__resolveType', moduleName: name, resolveReferenceType: true, @@ -266,17 +269,17 @@ export const executeGeneration = async (appPrefix = '~app', generatedPrefix = '~ generatedPrefix, }; const filePath = `${projectMainPath}/src/${graphqlFileRootPath}/types/`; - const fileName = `${interfaceName}${capitalizedFieldName}.ts`; + const fileName = `${resolverTypeName}${capitalizedFieldName}.ts`; const keepIfExists = true; saveRenderedTemplate(templateName, context, filePath, fileName, keepIfExists); }; - const createInterfaceSpec = (interfaceName: string) => { + const createResolveTypeSpec = (resoverTypeName: string) => { const templateName = './templates/typeTypeResolvers.spec.handlebars'; const capitalizedFieldName = capitalize('__resolveType'); const context = { - typeName: interfaceName, + typeName: resoverTypeName, fieldName: '__resolveType', moduleName: name, hasArguments: false, @@ -285,17 +288,17 @@ export const executeGeneration = async (appPrefix = '~app', generatedPrefix = '~ generatedPrefix, }; const filePath = `${projectMainPath}/src/${graphqlFileRootPath}/types/`; - const fileName = `${interfaceName}${capitalizedFieldName}.spec.ts`; + const fileName = `${resoverTypeName}${capitalizedFieldName}.spec.ts`; const keepIfExists = true; saveRenderedTemplate(templateName, context, filePath, fileName, keepIfExists); }; - const createInterfaceSpecWrapper = (interfaceName: string) => { + const createResolveTypeSpecWrapper = (resolverTypeName: string) => { const templateName = './templates/typeTypeResolversSpecWrapper.handlebars'; const capitalizedFieldName = capitalize('__resolveType'); const context = { - typeName: interfaceName, + typeName: resolverTypeName, fieldName: '__resolveType', moduleName: name, hasArguments: false, @@ -306,20 +309,31 @@ export const executeGeneration = async (appPrefix = '~app', generatedPrefix = '~ graphqlFileRootPath, }; const filePath = `${projectMainPath}/generated/graphql/helpers/`; - const fileName = `${interfaceName}${capitalizedFieldName}SpecWrapper.ts`; + const fileName = `${resolverTypeName}${capitalizedFieldName}SpecWrapper.ts`; const keepIfExists = false; saveRenderedTemplate(templateName, context, filePath, fileName, keepIfExists); }; interfaces.forEach((interfaceName) => { - createInterfaceType(interfaceName); - createInterfaceSpec(interfaceName); - createInterfaceSpecWrapper(interfaceName); + createResolveType(interfaceName); + createResolveTypeSpec(interfaceName); + createResolveTypeSpecWrapper(interfaceName); typeResolvers.push({ typeName: interfaceName, fieldName: [{ name: '__resolveType', capitalizedName: capitalize('__resolveType') }], }); }); + + unions.forEach((unionName) => { + createResolveType(unionName); + createResolveTypeSpec(unionName); + createResolveTypeSpecWrapper(unionName); + typeResolvers.push({ + typeName: unionName, + fieldName: [{ name: '__resolveType', capitalizedName: capitalize('__resolveType') }], + }); + }); + type FilteredType = { name: { value: string }; resolveReferenceType: boolean; arguments?: string[] }; typeDefinitions.forEach((typeDef) => { let filtered: FilteredType[] = []; @@ -333,7 +347,10 @@ export const executeGeneration = async (appPrefix = '~app', generatedPrefix = '~ filtered = type.astNode.fields.filter((field) => field.directives.find( (d: { name: { value: string } }) => - d.name.value === 'computed' || d.name.value === 'link' || d.name.value === 'requires', + d.name.value === 'computed' || + d.name.value === 'link' || + d.name.value === 'requires' || + d.name.value === 'map', ), ); } diff --git a/src/generate/parse-graphql/getInterfaces.spec.ts b/src/generate/parse-graphql/getInterfaces.spec.ts index 7dd5a90f..f434be65 100644 --- a/src/generate/parse-graphql/getInterfaces.spec.ts +++ b/src/generate/parse-graphql/getInterfaces.spec.ts @@ -31,3 +31,34 @@ test('get the interfaces', () => { expect(res).toEqual(['Home']); }); +test('should throw error if duplicate interface names are found', () => { + const schemaString = gql` + type TodoItem @key(fields: "id") { + id: ID! + list: List + } + + interface Home { + address: string + } + + extend type List { + id: ID! + todos: [TodoItem!]! + incompleteCount: Int! + } + + type InMemory { + id: ID! + } + + type Query { + homes: [Home] + } + interface Home { + address: string + } + `; + + expect(() => getInterfaces(schemaString)).toThrow('Duplicate interface name found: Home'); +}); diff --git a/src/generate/parse-graphql/getInterfaces.ts b/src/generate/parse-graphql/getInterfaces.ts index f1fc4070..4d428c95 100644 --- a/src/generate/parse-graphql/getInterfaces.ts +++ b/src/generate/parse-graphql/getInterfaces.ts @@ -1,14 +1,19 @@ import gql from 'graphql-tag'; +import validateUniqueName from './validateUniqueName'; export default (graphqlString: string) => { const graphqlAST = gql` ${graphqlString} `; - return ( - graphqlAST.definitions - .filter((d) => ['InterfaceTypeDefinition'].indexOf(d.kind) > -1) - // @ts-ignore - .map((f) => f.name.value) - ); + const interfacesTypeDefs = graphqlAST.definitions + .filter((d) => ['InterfaceTypeDefinition'].indexOf(d.kind) > -1) + // @ts-ignore + .map((f) => f.name.value); + + validateUniqueName(interfacesTypeDefs, (name: string) => { + throw new Error(`Duplicate interface name found: ${name}`); + }); + + return interfacesTypeDefs; }; diff --git a/src/generate/parse-graphql/getUnions.spec.ts b/src/generate/parse-graphql/getUnions.spec.ts new file mode 100644 index 00000000..1505712d --- /dev/null +++ b/src/generate/parse-graphql/getUnions.spec.ts @@ -0,0 +1,51 @@ +import getUnions from './getUnions'; + +const gql = (a: TemplateStringsArray) => a[0]; + +test('get the unions', () => { + const schemaString = gql` + type Query { + homes: [Home] + } + + type Cottage { + id: ID! + address: String! + } + + type Villa { + id: ID! + address: String! + owner: String! + } + + union Home = Cottage | Villa + `; + + const res = getUnions(schemaString); + + expect(res).toEqual(['Home']); +}); +test('should throw exception if duplicate union names are found', () => { + const schemaString = gql` + type Query { + homes: [Home] + } + + type Cottage { + id: ID! + address: String! + } + + type Villa { + id: ID! + address: String! + owner: String! + } + + union Home = Cottage | Villa + union Home = Cottage | Villa + `; + + expect(() => getUnions(schemaString)).toThrow('Duplicate union name found: Home'); +}); diff --git a/src/generate/parse-graphql/getUnions.ts b/src/generate/parse-graphql/getUnions.ts new file mode 100644 index 00000000..1613fdc7 --- /dev/null +++ b/src/generate/parse-graphql/getUnions.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag'; +import validateUniqueName from './validateUniqueName'; + +export default (graphqlString: string) => { + const graphqlAST = gql` + ${graphqlString} + `; + + const unionTypeDefinitions = graphqlAST.definitions + .filter((d) => ['UnionTypeDefinition'].indexOf(d.kind) > -1) + // @ts-ignore + .map((f) => f.name.value); + + validateUniqueName(unionTypeDefinitions, (name: string) => { + throw new Error(`Duplicate union name found: ${name}`); + }); + return unionTypeDefinitions; +}; diff --git a/src/generate/parse-graphql/validateUniqueName.spec.ts b/src/generate/parse-graphql/validateUniqueName.spec.ts new file mode 100644 index 00000000..003d58b1 --- /dev/null +++ b/src/generate/parse-graphql/validateUniqueName.spec.ts @@ -0,0 +1,19 @@ +import validateUniqueName from './validateUniqueName'; + +test('should throw error if duplicate entry found', () => { + const names = ['apple', 'banana', 'orange', 'banana']; + expect(() => + validateUniqueName(names, (name: any) => { + throw new Error(`duplicate record found: ${name}`); + }), + ).toThrow('duplicate record found: banana'); +}); + +test('should not throw error if no duplicate entry found', () => { + const names = ['apple', 'banana', 'orange']; + expect(() => + validateUniqueName(names, () => { + throw new Error('duplicate record found'); + }), + ).not.toThrow(); +}); diff --git a/src/generate/parse-graphql/validateUniqueName.ts b/src/generate/parse-graphql/validateUniqueName.ts new file mode 100644 index 00000000..d40536ce --- /dev/null +++ b/src/generate/parse-graphql/validateUniqueName.ts @@ -0,0 +1,8 @@ +export default function (names: string[], exceptionHandler: Function) { + const nameCounts: Map = new Map(); + names.forEach((name) => { + const val = (nameCounts.get(name) ?? 0) + 1; + if (val > 1) exceptionHandler(name); + nameCounts.set(name, val); + }); +}