-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Changes from all commits
d1ebd1b
396ff8a
8bd9e0d
ad16407
5b466ad
6875363
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,8 +9,10 @@ | |
"pretest": "yarn build", | ||
"test": "codegen-templates-scripts test" | ||
}, | ||
"dependencies": { | ||
"graphql-codegen-typescript-template": "0.12.6" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
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 |
---|---|---|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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>; | ||
} | ||
`); | ||
}); | ||
}); |
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); | ||
} |
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; |
There was a problem hiding this comment.
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