Skip to content

Commit

Permalink
add support for useImplementingTypes
Browse files Browse the repository at this point in the history
  • Loading branch information
maordaniel committed Mar 23, 2023
1 parent ec5ada0 commit 5b10387
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 26 deletions.
104 changes: 78 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ASTKindToNode, ListTypeNode, NamedTypeNode, parse, printSchema, TypeNode } from 'graphql';
import {
parse,
printSchema,
TypeNode,
ASTKindToNode,
ListTypeNode,
NamedTypeNode,
ObjectTypeDefinitionNode,
} from 'graphql';
import { faker } from '@faker-js/faker';
import casual from 'casual';
import { oldVisit, PluginFunction, resolveExternalModuleAndFn } from '@graphql-codegen/plugin-helpers';
Expand Down Expand Up @@ -26,6 +34,7 @@ type Options<T = TypeNode> = {
generateLibrary: 'casual' | 'faker';
fieldGeneration?: TypeFieldMap;
enumsAsTypes?: boolean;
useImplementingTypes: boolean;
};

const convertName = (value: string, fn: (v: string) => string, transformUnderscore: boolean): string => {
Expand Down Expand Up @@ -230,6 +239,16 @@ const handleValueGeneration = (
return baseGenerator();
};

const getNamedImplementType = (opts: Options<TypeItem['types']>): string => {
if (!opts.currentType || !('name' in opts.currentType)) {
return '';
}

const name = opts.currentType.name.value;
const casedName = createNameConverter(opts.typeNamesConvention, opts.transformUnderscore)(name);
return `${toMockName(name, casedName, opts.prefix)}()`;
};

const getNamedType = (opts: Options<NamedTypeNode>): string | number | boolean => {
if (!opts.currentType) {
return '';
Expand Down Expand Up @@ -264,8 +283,14 @@ const getNamedType = (opts: Options<NamedTypeNode>): string | number | boolean =
return handleValueGeneration(opts, customScalar, mockValueGenerator.integer);
}
default: {
const foundType = opts.types.find((enumType: TypeItem) => enumType.name === name);
if (foundType) {
const foundTypes = opts.types.filter((enumType: TypeItem) => {
if (enumType.types && 'interfaces' in enumType.types)
return enumType.types.interfaces.every((item) => item.name.value === name);
return enumType.name === name;
});

if (foundTypes.length) {
const foundType = foundTypes[0];
switch (foundType.type) {
case 'enum': {
// It's an enum
Expand Down Expand Up @@ -300,23 +325,27 @@ const getNamedType = (opts: Options<NamedTypeNode>): string | number | boolean =
foundType.name === 'Date' ? mockValueGenerator.date : mockValueGenerator.word,
);
}
case 'implement':
return foundTypes
.map((implementType: TypeItem) =>
getNamedImplementType({
...opts,
currentType: implementType.types,
}),
)
.join(' || ');
default:
throw `foundType is unknown: ${foundType.name}: ${foundType.type}`;
}
}
if (opts.terminateCircularRelationships) {
return handleValueGeneration(
opts,
null,
() =>
`relationshipsToOmit.has('${casedName}') ? {} as ${casedName} : ${toMockName(
name,
casedName,
opts.prefix,
)}({}, relationshipsToOmit)`,
);
return `relationshipsToOmit.has('${casedName}') ? {} as ${casedName} : ${toMockName(
name,
casedName,
opts.prefix,
)}({}, relationshipsToOmit)`;
} else {
return handleValueGeneration(opts, null, () => `${toMockName(name, casedName, opts.prefix)}()`);
return `${toMockName(name, casedName, opts.prefix)}()`;
}
}
}
Expand Down Expand Up @@ -470,13 +499,14 @@ export interface TypescriptMocksPluginConfig {
fieldGeneration?: TypeFieldMap;
locale?: string;
enumsAsTypes?: boolean;
useImplementingTypes?: boolean;
}

interface TypeItem {
name: string;
type: 'enum' | 'scalar' | 'union';
type: 'enum' | 'scalar' | 'union' | 'implement';
values?: string[];
types?: readonly NamedTypeNode[];
types?: readonly NamedTypeNode[] | ObjectTypeDefinitionNode;
}

type VisitFn<TAnyNode, TVisitedNode = TAnyNode> = (
Expand Down Expand Up @@ -516,14 +546,15 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
const dynamicValues = !!config.dynamicValues;
const generateLibrary = config.generateLibrary || 'casual';
const enumsAsTypes = config.enumsAsTypes ?? false;
const useImplementingTypes = config.useImplementingTypes ?? false;

if (generateLibrary === 'faker' && config.locale) {
faker.setLocale(config.locale);
}

// List of types that are enums
const types: TypeItem[] = [];
const visitor: VisitorType = {
const typeVisitor: VisitorType = {
EnumTypeDefinition: (node) => {
const name = node.name.value;
if (!types.find((enumType: TypeItem) => enumType.name === name)) {
Expand All @@ -544,6 +575,32 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
});
}
},
ObjectTypeDefinition: (node) => {
// This function triggered per each type
const typeName = node.name.value;

if (config.useImplementingTypes) {
if (!types.find((enumType) => enumType.name === typeName)) {
node.interfaces.length &&
types.push({
name: typeName,
type: 'implement',
types: node,
});
}
}
},
ScalarTypeDefinition: (node) => {
const name = node.name.value;
if (!types.find((enumType) => enumType.name === name)) {
types.push({
name,
type: 'scalar',
});
}
},
};
const visitor: VisitorType = {
FieldDefinition: (node) => {
const fieldName = node.name.value;

Expand All @@ -568,6 +625,7 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
generateLibrary,
fieldGeneration: config.fieldGeneration,
enumsAsTypes,
useImplementingTypes,
});

return ` ${fieldName}: overrides && overrides.hasOwnProperty('${fieldName}') ? overrides.${fieldName}! : ${value},`;
Expand Down Expand Up @@ -601,6 +659,7 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
generateLibrary,
fieldGeneration: config.fieldGeneration,
enumsAsTypes,
useImplementingTypes,
});

return ` ${field.name.value}: overrides && overrides.hasOwnProperty('${field.name.value}') ? overrides.${field.name.value}! : ${value},`;
Expand Down Expand Up @@ -665,17 +724,10 @@ export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, docu
},
};
},
ScalarTypeDefinition: (node) => {
const name = node.name.value;
if (!types.find((enumType) => enumType.name === name)) {
types.push({
name,
type: 'scalar',
});
}
},
};

// run on the types first
oldVisit(astNode, { leave: typeVisitor });
const result = oldVisit(astNode, { leave: visitor });
const definitions = result.definitions.filter((definition: any) => !!definition);
const typesFile = config.typesFile ? config.typesFile.replace(/\.[\w]+$/, '') : null;
Expand Down
83 changes: 83 additions & 0 deletions tests/useImplementingTypes/__snapshots__/spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should support useImplementingTypes 1`] = `
"
export const mockAConfig = (overrides?: Partial<AConfig>): AConfig => {
return {
configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test],
};
};
export const mockA = (overrides?: Partial<A>): A => {
return {
id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'cae147b0-1c04-459e-82db-624dd87433b4',
str: overrides && overrides.hasOwnProperty('str') ? overrides.str! : 'ea',
obj: overrides && overrides.hasOwnProperty('obj') ? overrides.obj! : mockB(),
config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockTestAConfig() || mockTestTwoAConfig(),
};
};
export const mockB = (overrides?: Partial<B>): B => {
return {
int: overrides && overrides.hasOwnProperty('int') ? overrides.int! : 696,
flt: overrides && overrides.hasOwnProperty('flt') ? overrides.flt! : 7.55,
bool: overrides && overrides.hasOwnProperty('bool') ? overrides.bool! : false,
};
};
export const mockTestAConfig = (overrides?: Partial<TestAConfig>): TestAConfig => {
return {
detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test],
active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true,
};
};
export const mockTestTwoAConfig = (overrides?: Partial<TestTwoAConfig>): TestTwoAConfig => {
return {
detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test],
username: overrides && overrides.hasOwnProperty('username') ? overrides.username! : 'et',
};
};
"
`;
exports[`shouldn't support useImplementingTypes 1`] = `
"
export const mockAConfig = (overrides?: Partial<AConfig>): AConfig => {
return {
configTypes: overrides && overrides.hasOwnProperty('configTypes') ? overrides.configTypes! : [ConfigTypes.Test],
};
};
export const mockA = (overrides?: Partial<A>): A => {
return {
id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'cae147b0-1c04-459e-82db-624dd87433b4',
str: overrides && overrides.hasOwnProperty('str') ? overrides.str! : 'ea',
obj: overrides && overrides.hasOwnProperty('obj') ? overrides.obj! : mockB(),
config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockAConfig(),
};
};
export const mockB = (overrides?: Partial<B>): B => {
return {
int: overrides && overrides.hasOwnProperty('int') ? overrides.int! : 696,
flt: overrides && overrides.hasOwnProperty('flt') ? overrides.flt! : 7.55,
bool: overrides && overrides.hasOwnProperty('bool') ? overrides.bool! : false,
};
};
export const mockTestAConfig = (overrides?: Partial<TestAConfig>): TestAConfig => {
return {
detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test],
active: overrides && overrides.hasOwnProperty('active') ? overrides.active! : true,
};
};
export const mockTestTwoAConfig = (overrides?: Partial<TestTwoAConfig>): TestTwoAConfig => {
return {
detectionTypes: overrides && overrides.hasOwnProperty('detectionTypes') ? overrides.detectionTypes! : [ConfigTypes.Test],
username: overrides && overrides.hasOwnProperty('username') ? overrides.username! : 'et',
};
};
"
`;
35 changes: 35 additions & 0 deletions tests/useImplementingTypes/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { buildSchema } from 'graphql';

export default buildSchema(/* GraphQL */ `
interface AConfig {
configTypes: [configTypes!]!
}
enum configTypes {
TEST
TEST2
}
type A {
id: ID!
str: String!
obj: B!
config: AConfig!
}
type B {
int: Int!
flt: Float!
bool: Boolean!
}
type TestAConfig implements AConfig {
detectionTypes: [configTypes!]!
active: Boolean!
}
type TestTwoAConfig implements AConfig {
detectionTypes: [configTypes!]!
username: String!
}
`);
26 changes: 26 additions & 0 deletions tests/useImplementingTypes/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { plugin } from '../../src';
import testSchema from './schema';

it('should support useImplementingTypes', async () => {
const result = await plugin(testSchema, [], { prefix: 'mock', useImplementingTypes: true });

expect(result).toBeDefined();
// Boolean
expect(result).toContain(
"config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockTestAConfig() || mockTestTwoAConfig(),",
);

expect(result).toMatchSnapshot();
});

it(`shouldn't support useImplementingTypes`, async () => {
const result = await plugin(testSchema, [], { prefix: 'mock' });

expect(result).toBeDefined();
// Boolean
expect(result).toContain(
"config: overrides && overrides.hasOwnProperty('config') ? overrides.config! : mockAConfig(),",
);

expect(result).toMatchSnapshot();
});

0 comments on commit 5b10387

Please sign in to comment.