From d1ebd1b8416d1fef444e915dc0f8bd20b50422b5 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 29 Oct 2018 14:57:33 +0100 Subject: [PATCH 1/6] test: allow to define custom parent type --- .../tests/resolvers.spec.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts index 6999ef63adb..785f8ef09c6 100644 --- a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts +++ b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts @@ -424,4 +424,93 @@ describe('Resolvers', () => { export type UserNameResolver = Resolver; `); }); + + it('should accept a map of parent types', async () => { + const { context } = compileAndBuildContext(` + type Query { + post: Post + } + + type Post { + id: String + author: User + } + + type User { + id: String + name: String + post: Post + } + + schema { + query: Query + } + `); + + // type UserParent = string; + // interface PostParent { + // id: string; + // author: string; + // } + const compiled = await compileTemplate( + { + ...config, + parentTypes: { + // it means that User type expects UserParent to be a parent + User: './interfaces#UserParent', + // it means that Post type expects UserParent to be a parent + Post: './interfaces#PostParent' + } + } as any, + context + ); + + const content = compiled[0].content; + + // TODO: import parents + // TODO: merge duplicates into single module + expect(content).toBeSimilarStringTo(` + import { UserParent, PostParent } from './interfaces'; + `); + + // TODO: should check field's result and match it with provided parents + expect(content).toBeSimilarStringTo(` + export namespace QueryResolvers { + export interface Resolvers { + post?: PostResolver; + } + + export type PostResolver = Resolver; + } + `); + + // TODO: should check if type has a defined parent and use it as TypeParent + expect(content).toBeSimilarStringTo(` + export namespace PostResolvers { + export interface Resolvers { + id?: IdResolver; + author?: AuthorResolver; + } + + export type IdResolver = Resolver; + export type AuthorResolver = Resolver; + } + `); + + // TODO: should check if type has a defined parent and use it as TypeParent + // TODO: should match field's result with provided parent type + expect(content).toBeSimilarStringTo(` + export namespace UserResolvers { + export interface Resolvers { + id?: IdResolver; + name?: NameResolver; + post?: PostResolver; + } + + export type IdResolver = Resolver; + export type NameResolver = Resolver; + export type NameResolver = Resolver; + } + `); + }); }); From 396ff8a388367a6e2a51f55a05ef293502cd720b Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 29 Oct 2018 16:12:11 +0100 Subject: [PATCH 2/6] Use Mappers for fields and types --- .../typescript-resolvers/src/config.ts | 2 + .../src/helpers/field-type.ts | 11 ++++ .../src/helpers/mappers.ts | 39 ++++++++++++++ .../src/helpers/parent-type.ts | 4 +- .../src/resolver.handlebars | 5 +- .../tests/resolvers.spec.ts | 54 ++++++++++--------- .../typescript/src/helpers/get-type.ts | 4 +- packages/templates/typescript/src/index.ts | 1 + .../typescript/src/utils/get-result-type.ts | 30 ++++++----- 9 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 packages/templates/typescript-resolvers/src/helpers/field-type.ts create mode 100644 packages/templates/typescript-resolvers/src/helpers/mappers.ts diff --git a/packages/templates/typescript-resolvers/src/config.ts b/packages/templates/typescript-resolvers/src/config.ts index 60b07fe14b5..11a88e48fb1 100644 --- a/packages/templates/typescript-resolvers/src/config.ts +++ b/packages/templates/typescript-resolvers/src/config.ts @@ -4,10 +4,12 @@ import * as resolver from './resolver.handlebars'; import * as beforeSchema from './before-schema.handlebars'; import * as afterSchema from './after-schema.handlebars'; import { getParentType } from './helpers/parent-type'; +import { getFieldType } from './helpers/field-type'; typescriptConfig.templates['resolver'] = resolver; typescriptConfig.templates['schema'] = `${beforeSchema}${typescriptConfig.templates['schema']}${afterSchema}`; typescriptConfig.customHelpers.getParentType = getParentType; +typescriptConfig.customHelpers.getFieldType = getFieldType; typescriptConfig.outFile = 'resolvers-types.ts'; export { typescriptConfig as config }; diff --git a/packages/templates/typescript-resolvers/src/helpers/field-type.ts b/packages/templates/typescript-resolvers/src/helpers/field-type.ts new file mode 100644 index 00000000000..44f9b21c649 --- /dev/null +++ b/packages/templates/typescript-resolvers/src/helpers/field-type.ts @@ -0,0 +1,11 @@ +import { Field } from 'graphql-codegen-core'; +import { convertedType, getFieldType as fieldType } from 'graphql-codegen-typescript-template'; +import { pickMapper } from './mappers'; + +export function getFieldType(field: Field, options: Handlebars.HelperOptions) { + const config = options.data.root.config || {}; + const mapper = pickMapper(field.type, config.mappers || {}); + const type: string = mapper ? fieldType(field, mapper.type, options) : convertedType(field, options); + + return type; +} diff --git a/packages/templates/typescript-resolvers/src/helpers/mappers.ts b/packages/templates/typescript-resolvers/src/helpers/mappers.ts new file mode 100644 index 00000000000..ce88bac1fc0 --- /dev/null +++ b/packages/templates/typescript-resolvers/src/helpers/mappers.ts @@ -0,0 +1,39 @@ +export interface ParentsMap { + [key: string]: string; +} + +export interface Mapper { + isExternal: boolean; + type: string; + source?: string; +} + +function isExternal(value: string) { + return value.includes('#'); +} + +function parseMapper(mapper: string): Mapper { + if (isExternal(mapper)) { + const [source, type] = mapper.split('#'); + return { + isExternal: true, + source, + type + }; + } + + return { + isExternal: false, + type: mapper + }; +} + +export function pickMapper(name: string, map: ParentsMap): Mapper | undefined { + const mapper = map[name]; + + if (!mapper) { + return undefined; + } + + return parseMapper(mapper); +} diff --git a/packages/templates/typescript-resolvers/src/helpers/parent-type.ts b/packages/templates/typescript-resolvers/src/helpers/parent-type.ts index 3a2af76c709..6dafe090677 100644 --- a/packages/templates/typescript-resolvers/src/helpers/parent-type.ts +++ b/packages/templates/typescript-resolvers/src/helpers/parent-type.ts @@ -1,5 +1,6 @@ import { Type, toPascalCase } from 'graphql-codegen-core'; import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { pickMapper } from './mappers'; function getRootTypeNames(schema: GraphQLSchema): string[] { const query = ((schema.getQueryType() || {}) as GraphQLObjectType).name; @@ -16,7 +17,8 @@ function isRootType(type: Type, schema: GraphQLSchema) { export function getParentType(type: Type, options: Handlebars.HelperOptions) { const config = options.data.root.config || {}; const schema: GraphQLSchema = options.data.root.rawSchema; - const name = `${config.interfacePrefix || ''}${toPascalCase(type.name)}`; + const mapper = pickMapper(type.name, config.mappers || {}); + const name = mapper ? mapper.type : `${config.interfacePrefix || ''}${toPascalCase(type.name)}`; return isRootType(type, schema) ? 'never' : name; } diff --git a/packages/templates/typescript-resolvers/src/resolver.handlebars b/packages/templates/typescript-resolvers/src/resolver.handlebars index 81889633275..abff2a067f8 100644 --- a/packages/templates/typescript-resolvers/src/resolver.handlebars +++ b/packages/templates/typescript-resolvers/src/resolver.handlebars @@ -5,12 +5,13 @@ export namespace {{ toPascalCase name }}Resolvers { export interface {{#if @root.config.noNamespaces}}{{ toPascalCase name }}{{/if}}Resolvers { {{#each fields}} {{ toComment description }} - {{ name }}?: {{#if @root.config.noNamespaces}}{{ toPascalCase ../name }}{{/if}}{{ getFieldResolverName name }}<{{ convertedType this }}, TypeParent, Context>; + {{ name }}?: {{#if @root.config.noNamespaces}}{{ toPascalCase ../name }}{{/if}}{{ getFieldResolverName name }}<{{ getFieldType this }}, TypeParent, Context>; {{/each}} } {{#each fields}} - export type {{#if @root.config.noNamespaces}}{{ toPascalCase ../name }}{{/if}}{{ getFieldResolverName name }} = {{ getFieldResolver this ../this }}; + + export type {{#if @root.config.noNamespaces}}{{ toPascalCase ../name }}{{/if}}{{ getFieldResolverName name }} = {{ getFieldResolver this ../this }}; {{~# if hasArguments }} diff --git a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts index 785f8ef09c6..679beece51d 100644 --- a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts +++ b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts @@ -455,11 +455,13 @@ describe('Resolvers', () => { const compiled = await compileTemplate( { ...config, - parentTypes: { - // it means that User type expects UserParent to be a parent - User: './interfaces#UserParent', - // it means that Post type expects UserParent to be a parent - Post: './interfaces#PostParent' + config: { + mappers: { + // it means that User type expects UserParent to be a parent + User: './interfaces#UserParent', + // it means that Post type expects UserParent to be a parent + Post: './interfaces#PostParent' + } } } as any, context @@ -467,49 +469,49 @@ describe('Resolvers', () => { const content = compiled[0].content; - // TODO: import parents - // TODO: merge duplicates into single module - expect(content).toBeSimilarStringTo(` - import { UserParent, PostParent } from './interfaces'; - `); + // import parents + // merge duplicates into single module + // expect(content).toBeSimilarStringTo(` + // import { UserParent, PostParent } from './interfaces'; + // `); - // TODO: should check field's result and match it with provided parents + // should check field's result and match it with provided parents expect(content).toBeSimilarStringTo(` export namespace QueryResolvers { export interface Resolvers { - post?: PostResolver; + post?: PostResolver; } - - export type PostResolver = Resolver; + + export type PostResolver = Resolver; } `); - // TODO: should check if type has a defined parent and use it as TypeParent + // should check if type has a defined parent and use it as TypeParent expect(content).toBeSimilarStringTo(` export namespace PostResolvers { - export interface Resolvers { + export interface Resolvers { id?: IdResolver; - author?: AuthorResolver; + author?: AuthorResolver; } - export type IdResolver = Resolver; - export type AuthorResolver = Resolver; + export type IdResolver = Resolver; + export type AuthorResolver = Resolver; } `); - // TODO: should check if type has a defined parent and use it as TypeParent - // TODO: should match field's result with provided parent type + // should check if type has a defined parent and use it as TypeParent + // should match field's result with provided parent type expect(content).toBeSimilarStringTo(` export namespace UserResolvers { - export interface Resolvers { + export interface Resolvers { id?: IdResolver; name?: NameResolver; - post?: PostResolver; + post?: PostResolver; } - export type IdResolver = Resolver; - export type NameResolver = Resolver; - export type NameResolver = Resolver; + export type IdResolver = Resolver; + export type NameResolver = Resolver; + export type PostResolver = Resolver; } `); }); diff --git a/packages/templates/typescript/src/helpers/get-type.ts b/packages/templates/typescript/src/helpers/get-type.ts index 1bfe72609f0..03aad7016de 100644 --- a/packages/templates/typescript/src/helpers/get-type.ts +++ b/packages/templates/typescript/src/helpers/get-type.ts @@ -1,5 +1,5 @@ import { SafeString } from 'handlebars'; -import { getResultType } from '../utils/get-result-type'; +import { convertedType } from '../utils/get-result-type'; import { Field } from 'graphql-codegen-core'; export function getType(type: Field, options: Handlebars.HelperOptions) { @@ -7,7 +7,7 @@ export function getType(type: Field, options: Handlebars.HelperOptions) { return ''; } - const result = getResultType(type, options); + const result = convertedType(type, options); return new SafeString(result); } diff --git a/packages/templates/typescript/src/index.ts b/packages/templates/typescript/src/index.ts index 28df1bde992..f6ee4d2f355 100644 --- a/packages/templates/typescript/src/index.ts +++ b/packages/templates/typescript/src/index.ts @@ -1,3 +1,4 @@ import { config } from './config'; +export { convertedType, getFieldType } from './utils/get-result-type'; export default config; diff --git a/packages/templates/typescript/src/utils/get-result-type.ts b/packages/templates/typescript/src/utils/get-result-type.ts index 9e8dff86256..85f276dd283 100644 --- a/packages/templates/typescript/src/utils/get-result-type.ts +++ b/packages/templates/typescript/src/utils/get-result-type.ts @@ -1,22 +1,16 @@ import { pascalCase } from 'change-case'; import { Field } from 'graphql-codegen-core'; -export function getResultType(type: Field, options: Handlebars.HelperOptions, skipPascalCase = false) { - const baseType = type.type; - const underscorePrefix = type.type.match(/^[\_]+/) || ''; +export function getFieldType(field: Field, realType: string, options: Handlebars.HelperOptions) { const config = options.data.root.config || {}; - const realType = - options.data.root.primitives[baseType] || - `${type.isScalar ? '' : config.interfacePrefix || ''}${underscorePrefix + - (skipPascalCase ? baseType : pascalCase(baseType))}`; const useImmutable = !!config.immutableTypes; - if (type.isArray) { + if (field.isArray) { let result = realType; - const dimension = type.dimensionOfArray + 1; + const dimension = field.dimensionOfArray + 1; - if (type.isNullableArray && !config.noNamespaces) { + if (field.isNullableArray && !config.noNamespaces) { result = useImmutable ? [realType, 'null'].join(' | ') : `(${[realType, 'null'].join(' | ')})`; } @@ -26,16 +20,28 @@ export function getResultType(type: Field, options: Handlebars.HelperOptions, sk result = `${result}${new Array(dimension).join('[]')}`; } - if (!type.isRequired) { + if (!field.isRequired) { result = [result, 'null'].join(' | '); } return result; } else { - if (type.isRequired) { + if (field.isRequired) { return realType; } else { return [realType, 'null'].join(' | '); } } } + +export function convertedType(type: Field, options: Handlebars.HelperOptions, skipPascalCase = false) { + const baseType = type.type; + const underscorePrefix = type.type.match(/^[\_]+/) || ''; + const config = options.data.root.config || {}; + const realType = + options.data.root.primitives[baseType] || + `${type.isScalar ? '' : config.interfacePrefix || ''}${underscorePrefix + + (skipPascalCase ? baseType : pascalCase(baseType))}`; + + return getFieldType(type, realType, options); +} From 8bd9e0dcaf3a7fc511be6dd3f557484ab0129991 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 29 Oct 2018 16:12:54 +0100 Subject: [PATCH 3/6] test: uncomment imports check --- .../templates/typescript-resolvers/tests/resolvers.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts index 679beece51d..5d4223b8e4a 100644 --- a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts +++ b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts @@ -471,9 +471,9 @@ describe('Resolvers', () => { // import parents // merge duplicates into single module - // expect(content).toBeSimilarStringTo(` - // import { UserParent, PostParent } from './interfaces'; - // `); + expect(content).toBeSimilarStringTo(` + import { UserParent, PostParent } from './interfaces'; + `); // should check field's result and match it with provided parents expect(content).toBeSimilarStringTo(` From ad16407e32623bf95c1205a549228e85acba1185 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 29 Oct 2018 16:32:08 +0100 Subject: [PATCH 4/6] Import mappers --- .../src/after-schema.handlebars | 1 + .../src/before-schema.handlebars | 3 ++ .../typescript-resolvers/src/config.ts | 2 + .../src/helpers/import-mappers.ts | 41 +++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 packages/templates/typescript-resolvers/src/helpers/import-mappers.ts diff --git a/packages/templates/typescript-resolvers/src/after-schema.handlebars b/packages/templates/typescript-resolvers/src/after-schema.handlebars index a1163526e28..cbf36ad49ca 100644 --- a/packages/templates/typescript-resolvers/src/after-schema.handlebars +++ b/packages/templates/typescript-resolvers/src/after-schema.handlebars @@ -1,4 +1,5 @@ {{{ blockCommentIf 'Resolvers' types }}} + {{#each types}} {{~> resolver }} {{/each}} \ No newline at end of file diff --git a/packages/templates/typescript-resolvers/src/before-schema.handlebars b/packages/templates/typescript-resolvers/src/before-schema.handlebars index e91683b2eff..192926cd3a4 100644 --- a/packages/templates/typescript-resolvers/src/before-schema.handlebars +++ b/packages/templates/typescript-resolvers/src/before-schema.handlebars @@ -25,3 +25,6 @@ export interface ISubscriptionResolverObject { export type SubscriptionResolver = | ((...args: any[]) => ISubscriptionResolverObject) | ISubscriptionResolverObject; + + +{{{ importMappers types }}} diff --git a/packages/templates/typescript-resolvers/src/config.ts b/packages/templates/typescript-resolvers/src/config.ts index 11a88e48fb1..a36982028d0 100644 --- a/packages/templates/typescript-resolvers/src/config.ts +++ b/packages/templates/typescript-resolvers/src/config.ts @@ -5,11 +5,13 @@ import * as beforeSchema from './before-schema.handlebars'; import * as afterSchema from './after-schema.handlebars'; import { getParentType } from './helpers/parent-type'; import { getFieldType } from './helpers/field-type'; +import { importMappers } from './helpers/import-mappers'; typescriptConfig.templates['resolver'] = resolver; typescriptConfig.templates['schema'] = `${beforeSchema}${typescriptConfig.templates['schema']}${afterSchema}`; typescriptConfig.customHelpers.getParentType = getParentType; typescriptConfig.customHelpers.getFieldType = getFieldType; +typescriptConfig.customHelpers.importMappers = importMappers; typescriptConfig.outFile = 'resolvers-types.ts'; export { typescriptConfig as config }; diff --git a/packages/templates/typescript-resolvers/src/helpers/import-mappers.ts b/packages/templates/typescript-resolvers/src/helpers/import-mappers.ts new file mode 100644 index 00000000000..f1979f778e5 --- /dev/null +++ b/packages/templates/typescript-resolvers/src/helpers/import-mappers.ts @@ -0,0 +1,41 @@ +import { Type } from 'graphql-codegen-core'; +import { pickMapper } from './mappers'; + +interface Modules { + [path: string]: string[]; +} + +export function importMappers(types: Type[], options: Handlebars.HelperOptions) { + const config = options.data.root.config || {}; + const mappers = config.mappers || {}; + const modules: Modules = {}; + const availableTypes = types.map(t => t.name); + + for (const type in mappers) { + if (mappers.hasOwnProperty(type)) { + const mapper = pickMapper(type, mappers); + + // checks if mapper comes from a module + // and if is used + if (mapper && mapper.isExternal && availableTypes.includes(type)) { + const path = mapper.source; + if (!modules[path]) { + modules[path] = []; + } + + // checks for duplicates + if (!modules[path].includes(mapper.type)) { + modules[path].push(mapper.type); + } + } + } + } + + const imports: string[] = Object.keys(modules).map( + path => ` + import { ${modules[path].join(', ')} } from '${path}'; + ` + ); + + return imports.join('\n'); +} From 5b466ad2bca0fb34078aac9a45a3f3b9af94a08c Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 29 Oct 2018 16:34:38 +0100 Subject: [PATCH 5/6] should accept mappers that reuse generated types --- .../tests/resolvers.spec.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts index 5d4223b8e4a..fc0a7003da2 100644 --- a/packages/templates/typescript-resolvers/tests/resolvers.spec.ts +++ b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts @@ -515,4 +515,57 @@ describe('Resolvers', () => { } `); }); + + it('should accept mappers that reuse generated types', async () => { + const { context } = compileAndBuildContext(` + type Query { + post: Post + } + + type Post { + id: String + } + + schema { + query: Query + } + `); + + const compiled = await compileTemplate( + { + ...config, + config: { + mappers: { + // it means that Post type expects Post to be a parent + Post: 'Post' + } + } + } as any, + context + ); + + const content = compiled[0].content; + + // should check field's result and match it with provided parents + expect(content).toBeSimilarStringTo(` + export namespace QueryResolvers { + export interface Resolvers { + post?: PostResolver; + } + + export type PostResolver = Resolver; + } + `); + + // should check if type has a defined parent and use it as TypeParent + expect(content).toBeSimilarStringTo(` + export namespace PostResolvers { + export interface Resolvers { + id?: IdResolver; + } + + export type IdResolver = Resolver; + } + `); + }); }); From 68753636ef29b436f5ef7ca8e38d179a5351f2f2 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Mon, 29 Oct 2018 16:42:28 +0100 Subject: [PATCH 6/6] Fix --- packages/templates/typescript-mongodb/package.json | 3 ++- .../templates/typescript-mongodb/src/helpers/entity-fields.ts | 4 ++-- packages/templates/typescript-resolvers/package.json | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/templates/typescript-mongodb/package.json b/packages/templates/typescript-mongodb/package.json index 9c330c4cb80..dff87379f0c 100644 --- a/packages/templates/typescript-mongodb/package.json +++ b/packages/templates/typescript-mongodb/package.json @@ -16,7 +16,8 @@ "graphql-codegen-core": "0.12.6" }, "dependencies": { - "lodash": "4.17.11" + "lodash": "4.17.11", + "graphql-codegen-typescript-template": "0.12.6" }, "main": "./dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/templates/typescript-mongodb/src/helpers/entity-fields.ts b/packages/templates/typescript-mongodb/src/helpers/entity-fields.ts index bd8aab5aa2a..33e512e8bd4 100644 --- a/packages/templates/typescript-mongodb/src/helpers/entity-fields.ts +++ b/packages/templates/typescript-mongodb/src/helpers/entity-fields.ts @@ -1,6 +1,6 @@ import { Field, Interface, Type } from 'graphql-codegen-core'; import { set } from 'lodash'; -import { getResultType } from '../../../typescript/src/utils/get-result-type'; +import { convertedType } from 'graphql-codegen-typescript-template'; // Directives fields const ID_DIRECTIVE = 'id'; @@ -27,7 +27,7 @@ function appendField(obj: object, field: string, value: string, mapDirectiveValu type FieldsResult = { [name: string]: string | FieldsResult }; function buildFieldDef(type: string, field: Field, options: Handlebars.HelperOptions): string { - return getResultType( + return convertedType( { ...field, type diff --git a/packages/templates/typescript-resolvers/package.json b/packages/templates/typescript-resolvers/package.json index 79f471960e1..ea3bcd5ff22 100644 --- a/packages/templates/typescript-resolvers/package.json +++ b/packages/templates/typescript-resolvers/package.json @@ -9,8 +9,10 @@ "pretest": "yarn build", "test": "codegen-templates-scripts test" }, + "dependencies": { + "graphql-codegen-typescript-template": "0.12.6" + }, "devDependencies": { - "graphql-codegen-typescript-template": "0.12.6", "codegen-templates-scripts": "0.12.6", "graphql-codegen-core": "0.12.6", "graphql-codegen-compiler": "0.12.6",