Skip to content

Commit

Permalink
feat: supports unions and adds validation for duplicate union and int…
Browse files Browse the repository at this point in the history
…erface.
  • Loading branch information
Bhavesh-Suvalaka authored and lgandecki committed Jul 20, 2022
1 parent 94427fe commit 1a814ce
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 18 deletions.
38 changes: 26 additions & 12 deletions src/generate/generate-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -247,36 +248,38 @@ 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');
const source = new Source(schemaString);
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,
capitalizedFieldName,
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,
Expand All @@ -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,
Expand All @@ -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[] = [];
Expand Down
31 changes: 31 additions & 0 deletions src/generate/parse-graphql/getInterfaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
17 changes: 11 additions & 6 deletions src/generate/parse-graphql/getInterfaces.ts
Original file line number Diff line number Diff line change
@@ -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;
};
51 changes: 51 additions & 0 deletions src/generate/parse-graphql/getUnions.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
18 changes: 18 additions & 0 deletions src/generate/parse-graphql/getUnions.ts
Original file line number Diff line number Diff line change
@@ -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;
};
19 changes: 19 additions & 0 deletions src/generate/parse-graphql/validateUniqueName.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
8 changes: 8 additions & 0 deletions src/generate/parse-graphql/validateUniqueName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function (names: string[], exceptionHandler: Function) {
const nameCounts: Map<string, number> = new Map();
names.forEach((name) => {
const val = (nameCounts.get(name) ?? 0) + 1;
if (val > 1) exceptionHandler(name);
nameCounts.set(name, val);
});
}

0 comments on commit 1a814ce

Please sign in to comment.