Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mappers (in typescript-resolvers template) #757

Merged
merged 6 commits into from
Oct 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/templates/typescript-mongodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it as a dependency because mongodb template depends on it

},
"main": "./dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed we use relative path instead of a package

import { convertedType } from 'graphql-codegen-typescript-template';

// Directives fields
const ID_DIRECTIVE = 'id';
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/templates/typescript-resolvers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
"pretest": "yarn build",
"test": "codegen-templates-scripts test"
},
"dependencies": {
"graphql-codegen-typescript-template": "0.12.6"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, we depend on it

},
"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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{{ blockCommentIf 'Resolvers' types }}}

{{#each types}}
{{~> resolver }}
{{/each}}
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ export interface ISubscriptionResolverObject<Result, Parent, Context, Args> {
export type SubscriptionResolver<Result, Parent = any, Context = any, Args = never> =
| ((...args: any[]) => ISubscriptionResolverObject<Result, Parent, Context, Args>)
| ISubscriptionResolverObject<Result, Parent, Context, Args>;


{{{ importMappers types }}}
4 changes: 4 additions & 0 deletions packages/templates/typescript-resolvers/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ 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';
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 };
11 changes: 11 additions & 0 deletions packages/templates/typescript-resolvers/src/helpers/field-type.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Collaborator Author

@kamilkisiela kamilkisiela Oct 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't emit unused types and there's no duplicates

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');
}
39 changes: 39 additions & 0 deletions packages/templates/typescript-resolvers/src/helpers/mappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export interface ParentsMap {
[key: string]: string;
}

export interface Mapper {
isExternal: boolean;
type: string;
source?: string;
}

function isExternal(value: string) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this says if Mapper comes from external module

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);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ export namespace {{ toPascalCase name }}Resolvers {
export interface {{#if @root.config.noNamespaces}}{{ toPascalCase name }}{{/if}}Resolvers<Context = {{#if @root.config.contextType}}{{@root.config.contextType}}{{else}}any{{/if}}, TypeParent = {{ getParentType this }}> {
{{#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 }}<R = {{ convertedType this }}, Parent = {{ getParentType ../this }}, Context = {{#if @root.config.contextType}}{{@root.config.contextType}}{{else}}any{{/if}}> = {{ getFieldResolver this ../this }};

export type {{#if @root.config.noNamespaces}}{{ toPascalCase ../name }}{{/if}}{{ getFieldResolverName name }}<R = {{ getFieldType this }}, Parent = {{ getParentType ../this }}, Context = {{#if @root.config.contextType}}{{@root.config.contextType}}{{else}}any{{/if}}> = {{ getFieldResolver this ../this }};

{{~# if hasArguments }}

Expand Down
144 changes: 144 additions & 0 deletions packages/templates/typescript-resolvers/tests/resolvers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,4 +424,148 @@ describe('Resolvers', () => {
export type UserNameResolver<R = string | null, Parent = User, Context = any> = Resolver<R, Parent, Context>;
`);
});

it('should accept a map of parent types', async () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it shows that fields and types get a mapper both in Result and Parent generics

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,
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
);

const content = compiled[0].content;

// import parents
// merge duplicates into single module
expect(content).toBeSimilarStringTo(`
import { UserParent, PostParent } from './interfaces';
`);

// should check field's result and match it with provided parents
expect(content).toBeSimilarStringTo(`
export namespace QueryResolvers {
export interface Resolvers<Context = any, TypeParent = never> {
post?: PostResolver<PostParent | null, TypeParent, Context>;
}

export type PostResolver<R = PostParent | null, Parent = never, Context = any> = Resolver<R, Parent, Context>;
}
`);

// should check if type has a defined parent and use it as TypeParent
expect(content).toBeSimilarStringTo(`
export namespace PostResolvers {
export interface Resolvers<Context = any, TypeParent = PostParent> {
id?: IdResolver<string | null, TypeParent, Context>;
author?: AuthorResolver<UserParent | null, TypeParent, Context>;
}

export type IdResolver<R = string | null, Parent = PostParent, Context = any> = Resolver<R, Parent, Context>;
export type AuthorResolver<R = UserParent | null, Parent = PostParent, Context = any> = Resolver<R, Parent, Context>;
}
`);

// 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<Context = any, TypeParent = UserParent> {
id?: IdResolver<string | null, TypeParent, Context>;
name?: NameResolver<string | null, TypeParent, Context>;
post?: PostResolver<PostParent | null, TypeParent, Context>;
}

export type IdResolver<R = string | null, Parent = UserParent, Context = any> = Resolver<R, Parent, Context>;
export type NameResolver<R = string | null, Parent = UserParent, Context = any> = Resolver<R, Parent, Context>;
export type PostResolver<R = PostParent | null, Parent = UserParent, Context = any> = Resolver<R, Parent, Context>;
}
`);
});

it('should accept mappers that reuse generated types', async () => {
Copy link
Collaborator Author

@kamilkisiela kamilkisiela Oct 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I show that we can reuse generated types and even use primitives or even ts things like Pick<> or Exclude<>

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<Context = any, TypeParent = never> {
post?: PostResolver<Post | null, TypeParent, Context>;
}

export type PostResolver<R = Post | null, Parent = never, Context = any> = Resolver<R, Parent, Context>;
}
`);

// should check if type has a defined parent and use it as TypeParent
expect(content).toBeSimilarStringTo(`
export namespace PostResolvers {
export interface Resolvers<Context = any, TypeParent = Post> {
id?: IdResolver<string | null, TypeParent, Context>;
}

export type IdResolver<R = string | null, Parent = Post, Context = any> = Resolver<R, Parent, Context>;
}
`);
});
});
4 changes: 2 additions & 2 deletions packages/templates/typescript/src/helpers/get-type.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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) {
if (!type) {
return '';
}

const result = getResultType(type, options);
const result = convertedType(type, options);

return new SafeString(result);
}
1 change: 1 addition & 0 deletions packages/templates/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { config } from './config';
export { convertedType, getFieldType } from './utils/get-result-type';

export default config;
Loading