From 59ac671a4158057bcb4ff4697b3c4ac31a6f92df Mon Sep 17 00:00:00 2001 From: Santino Puleio Date: Tue, 5 Oct 2021 23:47:28 +0200 Subject: [PATCH] Finalise transform --- .../replace-field/src/bareReplaceField.ts | 112 ------- .../transforms/replace-field/src/index.ts | 119 ++++++- .../replace-field/src/wrapReplaceField.ts | 33 -- ...pec.ts.snap => replace-field.spec.ts.snap} | 16 +- ...aceField.spec.ts => replace-field.spec.ts} | 25 +- .../replace-field/yaml-config.graphql | 13 +- packages/types/src/config-schema.json | 11 +- packages/types/src/config.ts | 8 +- website/docs/transforms/rename.md | 2 +- website/docs/transforms/replace-field.md | 308 +++++++++++++++++- .../transforms/transforms-introduction.md | 2 +- yarn.lock | 83 ++++- 12 files changed, 523 insertions(+), 209 deletions(-) delete mode 100644 packages/transforms/replace-field/src/bareReplaceField.ts delete mode 100644 packages/transforms/replace-field/src/wrapReplaceField.ts rename packages/transforms/replace-field/test/__snapshots__/{bareReplaceField.spec.ts.snap => replace-field.spec.ts.snap} (87%) rename packages/transforms/replace-field/test/{bareReplaceField.spec.ts => replace-field.spec.ts} (93%) diff --git a/packages/transforms/replace-field/src/bareReplaceField.ts b/packages/transforms/replace-field/src/bareReplaceField.ts deleted file mode 100644 index 510f63dff1a12..0000000000000 --- a/packages/transforms/replace-field/src/bareReplaceField.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { extendSchema, defaultFieldResolver, GraphQLFieldConfig, GraphQLFieldResolver, GraphQLSchema } from 'graphql'; -import { MeshTransform, MeshTransformOptions, SyncImportFn, YamlConfig } from '@graphql-mesh/types'; -import { MapperKind, mapSchema, selectObjectFields, pruneSchema } from '@graphql-tools/utils'; -import { loadFromModuleExportExpressionSync } from '@graphql-mesh/utils'; -import { loadTypedefsSync } from '@graphql-tools/load'; -import { CodeFileLoader } from '@graphql-tools/code-file-loader'; -import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; - -type ReplaceFieldConfig = YamlConfig.ReplaceFieldConfig & - Pick; - -// Execute original field resolver and return single property to be hoisted from rsesolver reponse -const defaultHoistFieldComposer = - (next: GraphQLFieldResolver, targetFieldName: string) => - async (root: any, args: any, context: any, info: any) => { - const rawResult = await next(root, args, context, info); - return rawResult && rawResult[targetFieldName]; - }; - -export default class BareReplaceField implements MeshTransform { - noWrap = true; - private baseDir: string; - private typeDefs: Pick; - private replacementsMap: Map; - private syncImportFn: SyncImportFn; - - constructor(options: MeshTransformOptions) { - const { baseDir, config, syncImportFn } = options; - this.baseDir = baseDir; - this.typeDefs = config.typeDefs; - this.replacementsMap = new Map(); - this.syncImportFn = syncImportFn; - - for (const replacement of config.replacements) { - const { - from: { type: fromTypeName, field: fromFieldName }, - to: toConfig, - scope, - composer, - } = replacement; - const fieldKey = `${fromTypeName}.${fromFieldName}`; - - const composerFn = loadFromModuleExportExpressionSync(composer, { - cwd: this.baseDir, - defaultExportName: 'default', - syncImportFn: this.syncImportFn, - }); - - this.replacementsMap.set(fieldKey, { ...toConfig, scope, composer: composerFn }); - } - } - - transformSchema(schema: GraphQLSchema) { - const additionalTypeDefs = - this.typeDefs && - loadTypedefsSync(this.typeDefs, { - cwd: this.baseDir, - loaders: [new CodeFileLoader(), new GraphQLFileLoader()], - }); - const baseSchema = additionalTypeDefs ? extendSchema(schema, additionalTypeDefs[0].document) : schema; - - const transformedSchema = mapSchema(baseSchema, { - [MapperKind.COMPOSITE_FIELD]: ( - fieldConfig: GraphQLFieldConfig, - fieldName: string, - typeName: string - ) => { - const fieldKey = `${typeName}.${fieldName}`; - const newFieldConfig = this.replacementsMap.get(fieldKey); - if (!newFieldConfig) { - return undefined; - } - - const targetFieldName = newFieldConfig.field; - const targetFieldConfig = selectObjectFields( - baseSchema, - newFieldConfig.type, - fieldName => fieldName === targetFieldName - )[targetFieldName]; - - if (newFieldConfig.scope === 'config') { - const targetResolver = targetFieldConfig.resolve; - targetFieldConfig.resolve = - newFieldConfig.composer && targetResolver ? newFieldConfig.composer(targetResolver) : targetResolver; - - // replace the entire field config - return [fieldName, targetFieldConfig]; - } - - // override field type with the target type requested - fieldConfig.type = targetFieldConfig.type; - - if (newFieldConfig.scope === 'hoistValue') { - // implement value hoisting by wrapping a default composer that hoists the value from resolver result - fieldConfig.resolve = defaultHoistFieldComposer(fieldConfig.resolve || defaultFieldResolver, targetFieldName); - } - - if (newFieldConfig.composer) { - // wrap user-defined composer to current field resolver or, if not preset, defaultFieldResolver - fieldConfig.resolve = newFieldConfig.composer(fieldConfig.resolve || defaultFieldResolver); - } - - // avoid re-iterating over replacements that have already been applied - this.replacementsMap.delete(fieldKey); - - return [fieldName, fieldConfig]; - }, - }); - - return pruneSchema(transformedSchema); - } -} diff --git a/packages/transforms/replace-field/src/index.ts b/packages/transforms/replace-field/src/index.ts index 64871630e125a..291cf89b7a31e 100644 --- a/packages/transforms/replace-field/src/index.ts +++ b/packages/transforms/replace-field/src/index.ts @@ -1,11 +1,112 @@ -import { YamlConfig, MeshTransformOptions } from '@graphql-mesh/types'; -import WrapReplaceField from './wrapReplaceField'; -import BareReplaceField from './bareReplaceField'; +import { extendSchema, defaultFieldResolver, GraphQLFieldConfig, GraphQLFieldResolver, GraphQLSchema } from 'graphql'; +import { MeshTransform, MeshTransformOptions, SyncImportFn, YamlConfig } from '@graphql-mesh/types'; +import { loadFromModuleExportExpressionSync } from '@graphql-mesh/utils'; +import { CodeFileLoader } from '@graphql-tools/code-file-loader'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import { loadTypedefsSync } from '@graphql-tools/load'; +import { MapperKind, mapSchema, selectObjectFields, pruneSchema } from '@graphql-tools/utils'; -interface ReplaceFieldTransformConstructor { - new (options: MeshTransformOptions): BareReplaceField | WrapReplaceField; -} +type ReplaceFieldConfig = YamlConfig.ReplaceFieldConfig & + Pick; + +// Execute original field resolver and return single property to be hoisted from rsesolver reponse +const defaultHoistFieldComposer = + (next: GraphQLFieldResolver, targetFieldName: string) => + async (root: any, args: any, context: any, info: any) => { + const rawResult = await next(root, args, context, info); + return rawResult && rawResult[targetFieldName]; + }; + +export default class ReplaceFieldTransform implements MeshTransform { + noWrap = true; + private baseDir: string; + private typeDefs: Pick; + private replacementsMap: Map; + private syncImportFn: SyncImportFn; + + constructor(options: MeshTransformOptions) { + const { baseDir, config, syncImportFn } = options; + this.baseDir = baseDir; + this.typeDefs = config.typeDefs; + this.replacementsMap = new Map(); + this.syncImportFn = syncImportFn; + + for (const replacement of config.replacements) { + const { + from: { type: fromTypeName, field: fromFieldName }, + to: toConfig, + scope, + composer, + } = replacement; + const fieldKey = `${fromTypeName}.${fromFieldName}`; + + const composerFn = loadFromModuleExportExpressionSync(composer, { + cwd: this.baseDir, + defaultExportName: 'default', + syncImportFn: this.syncImportFn, + }); + + this.replacementsMap.set(fieldKey, { ...toConfig, scope, composer: composerFn }); + } + } + + transformSchema(schema: GraphQLSchema) { + const additionalTypeDefs = + this.typeDefs && + loadTypedefsSync(this.typeDefs, { + cwd: this.baseDir, + loaders: [new CodeFileLoader(), new GraphQLFileLoader()], + }); + const baseSchema = additionalTypeDefs ? extendSchema(schema, additionalTypeDefs[0].document) : schema; + + const transformedSchema = mapSchema(baseSchema, { + [MapperKind.COMPOSITE_FIELD]: ( + fieldConfig: GraphQLFieldConfig, + fieldName: string, + typeName: string + ) => { + const fieldKey = `${typeName}.${fieldName}`; + const newFieldConfig = this.replacementsMap.get(fieldKey); + if (!newFieldConfig) { + return undefined; + } -export default (function ReplaceFieldTransform(options: MeshTransformOptions) { - return options.config.mode === 'bare' ? new BareReplaceField(options) : new WrapReplaceField(options); -} as unknown as ReplaceFieldTransformConstructor); + const targetFieldName = newFieldConfig.field; + const targetFieldConfig = selectObjectFields( + baseSchema, + newFieldConfig.type, + fieldName => fieldName === targetFieldName + )[targetFieldName]; + + if (newFieldConfig.scope === 'config') { + const targetResolver = targetFieldConfig.resolve; + targetFieldConfig.resolve = + newFieldConfig.composer && targetResolver ? newFieldConfig.composer(targetResolver) : targetResolver; + + // replace the entire field config + return [fieldName, targetFieldConfig]; + } + + // override field type with the target type requested + fieldConfig.type = targetFieldConfig.type; + + if (newFieldConfig.scope === 'hoistValue') { + // implement value hoisting by wrapping a default composer that hoists the value from resolver result + fieldConfig.resolve = defaultHoistFieldComposer(fieldConfig.resolve || defaultFieldResolver, targetFieldName); + } + + if (newFieldConfig.composer) { + // wrap user-defined composer to current field resolver or, if not preset, defaultFieldResolver + fieldConfig.resolve = newFieldConfig.composer(fieldConfig.resolve || defaultFieldResolver); + } + + // avoid re-iterating over replacements that have already been applied + this.replacementsMap.delete(fieldKey); + + return [fieldName, fieldConfig]; + }, + }); + + return pruneSchema(transformedSchema); + } +} diff --git a/packages/transforms/replace-field/src/wrapReplaceField.ts b/packages/transforms/replace-field/src/wrapReplaceField.ts deleted file mode 100644 index 618c597464df1..0000000000000 --- a/packages/transforms/replace-field/src/wrapReplaceField.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GraphQLSchema } from 'graphql'; -import { MeshTransform, MeshTransformOptions, YamlConfig } from '@graphql-mesh/types'; -import { DelegationContext } from '@graphql-tools/delegate'; -import { ExecutionRequest, ExecutionResult } from '@graphql-tools/utils'; - -export default class WrapHoistField implements MeshTransform { - constructor(options: MeshTransformOptions) { - // TODO - } - - transformSchema( - originalWrappingSchema: GraphQLSchema - // subschemaConfig: SubschemaConfig, - // transformedSchema?: GraphQLSchema - ) { - // TODO - return originalWrappingSchema; - } - - transformRequest( - originalRequest: ExecutionRequest - // delegationContext: DelegationContext, - // transformationContext: Record - ) { - // TODO - return originalRequest; - } - - transformResult(originalResult: ExecutionResult, delegationContext: DelegationContext, transformationContext: any) { - // TODO - return originalResult; - } -} diff --git a/packages/transforms/replace-field/test/__snapshots__/bareReplaceField.spec.ts.snap b/packages/transforms/replace-field/test/__snapshots__/replace-field.spec.ts.snap similarity index 87% rename from packages/transforms/replace-field/test/__snapshots__/bareReplaceField.spec.ts.snap rename to packages/transforms/replace-field/test/__snapshots__/replace-field.spec.ts.snap index 571a5c489c14a..e868b7f706ed7 100644 --- a/packages/transforms/replace-field/test/__snapshots__/bareReplaceField.spec.ts.snap +++ b/packages/transforms/replace-field/test/__snapshots__/replace-field.spec.ts.snap @@ -2,10 +2,10 @@ exports[`replace-field should replace correctly field Type with additional type definitions 1`] = ` "type Query { - books: [Books] + books: [Book] } -type Books { +type Book { title: String! author: Author! } @@ -19,10 +19,10 @@ type Author { exports[`replace-field should replace correctly field with resolver function 1`] = ` "type Query { - books: [Books] + books: [Book] } -type Books { +type Book { title: String! author: Author! } @@ -40,10 +40,10 @@ exports[`replace-field should replace correctly field without resolver function } type BooksApiResponse { - books: [Books] + books: [Book] } -type Books { +type Book { title: String! author: String! } @@ -52,10 +52,10 @@ type Books { exports[`replace-field should replace correctly mtultiple fields with and without resolver function 1`] = ` "type Query { - books: [Books] + books: [Book] } -type Books { +type Book { title: String! author: String! } diff --git a/packages/transforms/replace-field/test/bareReplaceField.spec.ts b/packages/transforms/replace-field/test/replace-field.spec.ts similarity index 93% rename from packages/transforms/replace-field/test/bareReplaceField.spec.ts rename to packages/transforms/replace-field/test/replace-field.spec.ts index 2224617140116..22c216ac4f9d7 100644 --- a/packages/transforms/replace-field/test/bareReplaceField.spec.ts +++ b/packages/transforms/replace-field/test/replace-field.spec.ts @@ -14,10 +14,10 @@ describe('replace-field', () => { } type BooksApiResponse { - books: [Books] + books: [Book] } - type Books { + type Book { title: String! author: Author! } @@ -39,7 +39,6 @@ describe('replace-field', () => { it('should replace correctly field with resolver function', async () => { const transform = new ReplaceFieldTransform({ config: { - mode: 'bare', replacements: [ { from: { @@ -73,7 +72,7 @@ describe('replace-field', () => { const transformedSchema = transform.transformSchema(schema); expect(transformedSchema.getType('BooksApiResponse')).toBeUndefined(); - expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Books]'); + expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Book]'); expect(printSchema(transformedSchema)).toMatchSnapshot(); const result = await execute({ @@ -92,11 +91,10 @@ describe('replace-field', () => { it('should replace correctly field without resolver function', async () => { const transform = new ReplaceFieldTransform({ config: { - mode: 'bare', replacements: [ { from: { - type: 'Books', + type: 'Book', field: 'author', }, to: { @@ -129,9 +127,7 @@ describe('replace-field', () => { const transformedSchema = transform.transformSchema(schema); expect(transformedSchema.getType('Author')).toBeUndefined(); - expect((transformedSchema.getType('Books') as GraphQLObjectType).getFields().author.type.toString()).toBe( - 'String!' - ); + expect((transformedSchema.getType('Book') as GraphQLObjectType).getFields().author.type.toString()).toBe('String!'); expect(printSchema(transformedSchema)).toMatchSnapshot(); const result = await execute({ @@ -156,7 +152,6 @@ describe('replace-field', () => { it('should replace correctly mtultiple fields with and without resolver function', async () => { const transform = new ReplaceFieldTransform({ config: { - mode: 'bare', replacements: [ { from: { @@ -171,7 +166,7 @@ describe('replace-field', () => { }, { from: { - type: 'Books', + type: 'Book', field: 'author', }, to: { @@ -205,10 +200,8 @@ describe('replace-field', () => { expect(transformedSchema.getType('BooksApiResponse')).toBeUndefined(); expect(transformedSchema.getType('Author')).toBeUndefined(); - expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Books]'); - expect((transformedSchema.getType('Books') as GraphQLObjectType).getFields().author.type.toString()).toBe( - 'String!' - ); + expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Book]'); + expect((transformedSchema.getType('Book') as GraphQLObjectType).getFields().author.type.toString()).toBe('String!'); expect(printSchema(transformedSchema)).toMatchSnapshot(); const result = await execute({ @@ -231,7 +224,6 @@ describe('replace-field', () => { it('should replace correctly field Type with additional type definitions', async () => { const transform = new ReplaceFieldTransform({ config: { - mode: 'bare', typeDefs: /* GraphQL */ ` type NewAuthor { age: String @@ -307,7 +299,6 @@ describe('replace-field', () => { it('should replace correctly field with composer wrapping resolver function', async () => { const transform = new ReplaceFieldTransform({ config: { - mode: 'bare', replacements: [ { from: { diff --git a/packages/transforms/replace-field/yaml-config.graphql b/packages/transforms/replace-field/yaml-config.graphql index 35e0aaea1ad75..ae79629f8afc7 100644 --- a/packages/transforms/replace-field/yaml-config.graphql +++ b/packages/transforms/replace-field/yaml-config.graphql @@ -2,14 +2,10 @@ extend type Transform { """ Transformer to replace GraphQL field with partial of full config from a different field """ - replaceField: ReplaceFieldTransform + replaceField: ReplaceFieldTransformConfig } -type ReplaceFieldTransform @md { - """ - Specify to apply ReplaceField transform to bare schema or by wrapping original schema - """ - mode: ReplaceFieldTransformMode +type ReplaceFieldTransformConfig @md { """ Additional type definition to used to replace field types """ @@ -36,8 +32,3 @@ type ReplaceFieldConfig { type: String! field: String! } - -enum ReplaceFieldTransformMode { - bare - wrap -} diff --git a/packages/types/src/config-schema.json b/packages/types/src/config-schema.json index 973b9933e9878..9623fa1040f91 100644 --- a/packages/types/src/config-schema.json +++ b/packages/types/src/config-schema.json @@ -363,7 +363,7 @@ "title": "Transform", "properties": { "replaceField": { - "$ref": "#/definitions/ReplaceFieldTransform", + "$ref": "#/definitions/ReplaceFieldTransformConfig", "description": "Transformer to replace GraphQL field with partial of full config from a different field" }, "cache": { @@ -1751,16 +1751,11 @@ } } }, - "ReplaceFieldTransform": { + "ReplaceFieldTransformConfig": { "additionalProperties": false, "type": "object", - "title": "ReplaceFieldTransform", + "title": "ReplaceFieldTransformConfig", "properties": { - "mode": { - "type": "string", - "enum": ["bare", "wrap"], - "description": "Specify to apply ReplaceField transform to bare schema or by wrapping original schema (Allowed values: bare, wrap)" - }, "typeDefs": { "anyOf": [ { diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 558f9e52920a9..ac4ab5e1d0b86 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -907,7 +907,7 @@ export interface TuqlHandler { infile?: string; } export interface Transform { - replaceField?: ReplaceFieldTransform; + replaceField?: ReplaceFieldTransformConfig; /** * Transformer to apply caching for your data sources */ @@ -937,11 +937,7 @@ export interface Transform { /** * Transformer to replace GraphQL field with partial of full config from a different field */ -export interface ReplaceFieldTransform { - /** - * Specify to apply ReplaceField transform to bare schema or by wrapping original schema (Allowed values: bare, wrap) - */ - mode?: 'bare' | 'wrap'; +export interface ReplaceFieldTransformConfig { /** * Additional type definition to used to replace field types */ diff --git a/website/docs/transforms/rename.md b/website/docs/transforms/rename.md index 3530f64a43eee..af18afb1b345e 100644 --- a/website/docs/transforms/rename.md +++ b/website/docs/transforms/rename.md @@ -4,7 +4,7 @@ title: Rename Transform sidebar_label: Rename --- -The `rename` transform allow you rename GraphQL types and GraphQL fields easily. +The `rename` transform allow you to rename GraphQL types and GraphQL fields easily. ``` yarn add @graphql-mesh/transform-rename diff --git a/website/docs/transforms/replace-field.md b/website/docs/transforms/replace-field.md index 06fae5de4796f..d797260187e05 100644 --- a/website/docs/transforms/replace-field.md +++ b/website/docs/transforms/replace-field.md @@ -4,4 +4,310 @@ title: Replace Field sidebar_label: Replace Field --- -TODO +The `replace-field` transform allows you to replace configuration properties of a GraphQL field (source) with the ones of another field (target). + +This is extremely useful when you want to hoist field values from one subfield to its parent, but it can be customised to completely replace and/or compose resolve functions with a great degree of customisation. + +``` +yarn add @graphql-mesh/transform-replace-field +``` + +> Note: currently this transform supports `bare` mode only. For information about "bare" and "wrap" modes, please read the [dedicated section](/docs/transforms/transforms-introduction#two-different-modes). + +## How to use? + +Imagine you have generated your schema from a data source you don't control, and the generated schema looks like this: + +```graphql +type Query { + books: BooksApiResponse +} + +type BooksApiResponse { + books: [Book] +} + +type Book { + title: String! + author: Author! + code: String +} + +type Author { + name: String! + age: Int! +} +``` + +As you can see you would have to request a GraphQL Document like the following, to retrieve the list of books: + +```graphql +{ + books { + books { + title + author + } + } +} +``` + +This is not ideal because you have to request `books` as a child `books`, so in this case hoisting the value from child to parent would lead to a cleaner schema and request Document. + +To achieve this you can add the following configuration to your Mesh config file: + +```yml +transforms: + - replace-field: + replacements: + - from: + type: Query + field: books + to: + type: BooksApiResponse + field: books + scope: hoistValue +``` + +This will transform your schema from what you had above, to this: + +```graphql +type Query { + books: [Book] +} + +type Book { + title: String! + author: Author! + code: String +} + +type Author { + name: String! + age: Int! +} +``` + +Allowing you to request a GraphQL document like this: + +```graphql +{ + books { + title + author + } +} +``` + +## How the transform works + +Let's understand more about how this transform works. +With `from` you define your source, which field in the schema you want to replace. + +```yaml +- from: + type: Query + field: books +``` + +In this case, we want to replace the field `books` in type `Query`, which by default has the type `BooksApiResponse`. + +With `to` you define your target, and so which field should replace your identified source field. + +```yaml +to: + type: BooksApiResponse + field: books +``` + +To summarise the configuration above we want field `books` in type `Query` to be replaced from being of type `BooksApiResponse` to become type `[Book]`. + +Finally, since we no longer have any reference to type `BooksApiResponse` this becomes a loose type, and so the transform will purge it from the GraphQL schema. + +## Transform scopes + +In the paragraph above we've explored how to use the transform to replace field Types. +The transform always replaces the type of the source field with the one of the target. + +However, the transform also allows you to pass a scope property, which values can be `config` or `hoistValue`. + +We could say that the scope property could also take a `type` value, but since it's the minimum requirement to replace the Type, this is considered the default scope and so it wouldn't make sense to pass it when you desire just this behaviour. + +### scope: config + +When you pass `config: scope` the transform will replace the full field config. +A field config includes properties of the field such as description, type, args, resolve, subscribe, deprecationReason, extensions, astNode. + +As you can see this is very comprehensive as it includes things like arguments as well as the resolve and subscribe functions. + +This can be useful when you have custom resolve functions on your target field and so you are happy to replace the source field entirely. +However, you should be careful in doing this when you fully understand the implications of the behaviour for your replaced field. + +### scope: hoistValue + +We have seen how `hoistValue` can be useful in the full example described in the "How to use?" paragraph. + +Once again, by default, the transform will replace the Type of the field only. +When passing `scope: hoistValue` in addition to replacing the Type, the transform will wrap the resolver of the original field (source) with a function. This function intercepts the return value of the resolver to ultimately return only the direct child property that has the same name as the target field; hence performing value hoisting. + +Taking into account the original schema shared above, originally `Query.books` would return a value like this: + +```js +{ + books: { + books: [{ title: 'abc', author: 'def' }, { title: 'ghi', author: 'lmn' }] + } +} +``` + +But the wrapping function applied to the original resolver, when passing `hoistValue` scope, will change the value above to this: + +```js +{ + books: [{ title: 'abc', author: 'def' }, { title: 'ghi', author: 'lmn' }] +} +``` + +## Additional type definitions + +The examples shared so far are simple because we wanted to replace fields with other fields that are available in the original schema. + +However, sometimes you might want to replace a field Type with something that is not available in the original schema. +In this case, the transform allows you to pass additional type definitions that will be injected into your schema so that you can then use them as target field Types. + +Let's have a look at a Mesh config to be applied to the GraphQL schema shared above: + +```yml +transforms: + - replace-field: + typeDefs: | + type NewAuthor { + age: String + } + # typeDefs: ./customTypeDefs.graphql # for conveniency, you can also pass a .graphql file + replacements: + - from: + type: Author + field: age + to: + type: NewAuthor + field: age +``` + +The config above will change type `Author` from this: + +```graphql +type Author { + name: String! + age: Int! +} +``` + +To this: + +```graphql +type Author { + name: String! + age: String +} +``` + +## Custom composers + +Performing value hoisting or replacing the full field config is powerful, but it might not always fully satisfy custom needs. +For instance, if you applied transforms to the bare schema (such as field renaming) the built-in value hoisting functionality won't work, because you'd need to hoist the child property provided by the original schema, and not the renamed version. + +The transform allows you to assign composers to each replace rule, which lets you define your custom logic on top of resolve functions. + +A composer is a function that wraps the resolve function, giving you access to this before it is executed. You can then intercept its output value so that finally you can also define a custom return value. + +Let's look at an example. +Currently, our `Book` type has a `code` field, we want to replace this field and turn it into a boolean. Our logic assumes that if we have a book code, it means this book is available in our store. +Eventually, we want to completely replace `code` with `isAvailable`; as you can see this requires implementing custom logic. + +```yml +transforms: + - replace-field: + typeDefs: | + type NewBook { + isAvailable: Boolean + } + replacements: + - from: + type: Book + field: code + to: + type: NewBook + field: isAvailable + composer: ./customResolvers.js#isAvailable +``` + +```js +// customResolvers.js + +module.exports = { + isAvailable: next => async (root, args, context, info) => { + // 'next' is the original resolve function + const code = await next(root, args, context, info); + return Boolean(code); + }, +}; +``` + +Now our `code` field will return a Boolean as per custom logic implemented through the javascript function above. + +We probably want to finish this up with an extra touch to rename the field `code` to `isAvailable`, even though that's a detail beyond the scope of the replace field transform. + +Let's wrap this up: + +```yml +transforms: + - replace-field: + typeDefs: | + type NewBook { + isAvailable: Boolean + } + replacements: + - from: + type: Query + field: books + to: + type: BooksApiResponse + field: books + scope: hoistValue + - from: + type: Book + field: code + to: + type: NewBook + field: isAvailable + composer: ./customResolvers.js#isAvailable + - rename: + mode: bare | wrap + renames: + - from: + type: Book + field: code + to: + type: Book + field: isAvailable +``` + +And now we have the following shiny GraphQL schema: + +```graphql +type Query { + books: [Book] +} + +type Book { + title: String! + author: Author! + isAvailable: Boolean +} + +type Author { + name: String! + age: Int! +} +``` diff --git a/website/docs/transforms/transforms-introduction.md b/website/docs/transforms/transforms-introduction.md index 2be2e04da7114..b305d90b38112 100644 --- a/website/docs/transforms/transforms-introduction.md +++ b/website/docs/transforms/transforms-introduction.md @@ -202,6 +202,6 @@ If you have use cases for which you would require to introduce either "bare" or | Naming Convention | ❌ | ✅ | [docs](/docs/transforms/naming-convention) | | Prefix | ✅ | ✅ | [docs](/docs/transforms/prefix) | | Rename | ✅ | ✅ | [docs](/docs/transforms/rename) | -| Replace Field | ✅ | ❓ | [docs](/docs/transforms/replace-field) | +| Replace Field | ✅ | ❌ | [docs](/docs/transforms/replace-field) | | Resolvers Composition | ✅ | ✅ | [docs](/docs/transforms/resolvers-composition) | | Snapshot | ✅ | ❌ | [docs](/docs/transforms/snapshot) | diff --git a/yarn.lock b/yarn.lock index cd053c0b41039..33f1ed2714da0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2645,6 +2645,39 @@ object-inspect "1.10.3" tslib "^2.0.0" +"@graphql-mesh/types@0.47.0": + version "0.47.0" + resolved "https://registry.yarnpkg.com/@graphql-mesh/types/-/types-0.47.0.tgz#2b188d987d9fde7132cc200a5c845ab9c448312d" + integrity sha512-VhIUghlVnWzAmHP29pqadrX6FAb8eCYnIJ7WY3eaZ4rNfLrfM/MCypJy/77l7G/n16ZzObQSRpmPRnRQ9HMpkg== + dependencies: + "@graphql-tools/delegate" "8.1.1" + "@graphql-tools/utils" "8.1.2" + "@graphql-typed-document-node/core" "3.1.0" + fetchache "0.1.1" + +"@graphql-mesh/utils@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@graphql-mesh/utils/-/utils-0.16.0.tgz#854bf674339fca1b3428d884b233c26da6efecb4" + integrity sha512-8jj22BeI/KJMfeXOQw043g8k8lvPYc/5yvG2tP9qADI5DEjz6nGMZZqTFXFPHN0UoyegTVS6U3mydSNYGc9clQ== + dependencies: + "@ardatan/string-interpolation" "1.2.12" + "@graphql-mesh/types" "0.47.0" + "@graphql-tools/delegate" "8.1.1" + "@graphql-tools/utils" "8.1.2" + "@graphql-typed-document-node/core" "3.1.0" + chalk "4.1.2" + cross-fetch "3.1.4" + date-fns "2.23.0" + fetchache "0.1.1" + flatstr "1.0.12" + graphql-jit "0.5.2" + graphql-subscriptions "1.2.1" + is-url "1.2.4" + js-yaml "4.1.0" + lodash "4.17.21" + object-hash "2.2.0" + tiny-lru "7.0.6" + "@graphql-tools/apollo-engine-loader@^6": version "6.2.5" resolved "https://registry.yarnpkg.com/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-6.2.5.tgz#b9e65744f522bb9f6ca50651e5622820c4f059a8" @@ -2683,7 +2716,7 @@ tslib "~2.2.0" value-or-promise "1.0.6" -"@graphql-tools/batch-execute@^8.1.1": +"@graphql-tools/batch-execute@^8.0.5", "@graphql-tools/batch-execute@^8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.1.1.tgz#16c62a141304f81b5eb102fcbadbcac500e2aee4" integrity sha512-euRh9Fxhrpb99sbPk++hPHEZ9c0zmLfh/1zOk2BMxWOic65WlNDuFsNV2S6ddlruu9PNL6dEpBteiTfRWxpMfw== @@ -2724,6 +2757,18 @@ is-promise "4.0.0" tslib "~2.0.1" +"@graphql-tools/delegate@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-8.1.1.tgz#d20e6d81a2900b1c8a69c2c0a3a8a0df2f9030c2" + integrity sha512-Vttd0nfYTqRnRMKLvk8s4cIi9U+OMXGc9CMZAlKkHrBJ6dGXjdSM+4n3p9rfWZc/FtbVk1FnNS4IFyMeKwFuxA== + dependencies: + "@graphql-tools/batch-execute" "^8.0.5" + "@graphql-tools/schema" "^8.1.2" + "@graphql-tools/utils" "^8.1.2" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.10" + "@graphql-tools/delegate@8.2.2", "@graphql-tools/delegate@^8.2.0": version "8.2.2" resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-8.2.2.tgz#35a4931d95160e4bd0ef5f21e63bfb182f03e5f9" @@ -2890,6 +2935,16 @@ unixify "1.0.0" valid-url "1.0.9" +"@graphql-tools/load@7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-7.3.0.tgz#dc4177bb4b7ae537c833a2fcd97ab07b6c789c65" + integrity sha512-ZVipT7yzOpf/DJ2sLI3xGwnULVFp/icu7RFEgDo2ZX0WHiS7EjWZ0cegxEm87+WN4fMwpiysRLzWx67VIHwKGA== + dependencies: + "@graphql-tools/schema" "8.2.0" + "@graphql-tools/utils" "^8.2.0" + p-limit "3.1.0" + tslib "~2.3.0" + "@graphql-tools/load@7.3.2": version "7.3.2" resolved "https://registry.yarnpkg.com/@graphql-tools/load/-/load-7.3.2.tgz#0edd9c0b464111968bfca148457a4c97e23fc030" @@ -3167,6 +3222,13 @@ dependencies: tslib "~2.3.0" +"@graphql-tools/utils@8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.1.2.tgz#a376259fafbca7532fda657e3abeec23b545e5d3" + integrity sha512-3G+NIBR5mHjPm78jAD0l07JRE0XH+lr9m7yL/wl69jAzK0Jr/H+/Ok4ljEolI70iglz+ZhIShVPAwyesF6rnFg== + dependencies: + tslib "~2.3.0" + "@graphql-tools/utils@8.2.2": version "8.2.2" resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.2.2.tgz#d29420bf1003d2876cb30f373145be432c7f7c4b" @@ -3174,7 +3236,7 @@ dependencies: tslib "~2.3.0" -"@graphql-tools/utils@8.2.4", "@graphql-tools/utils@^8.0.0", "@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.2.0", "@graphql-tools/utils@^8.2.2", "@graphql-tools/utils@^8.2.3", "@graphql-tools/utils@^8.2.4": +"@graphql-tools/utils@8.2.4", "@graphql-tools/utils@^8.0.0", "@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.1.2", "@graphql-tools/utils@^8.2.0", "@graphql-tools/utils@^8.2.2", "@graphql-tools/utils@^8.2.3", "@graphql-tools/utils@^8.2.4": version "8.2.4" resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.2.4.tgz#89f92c8dc0cdb6b9f32c90e904aa90e66a2e0a58" integrity sha512-uB+JL7CqTKJ2Q5zXA+a2la1cA8YYPcc0RHO/3mK54hxlZa2Z5/9k9XrNfMof4LZQefTaBM7M6QWtaxGklJln4A== @@ -9695,6 +9757,11 @@ dataloader@2.0.0, dataloader@^2.0.0: resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f" integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ== +date-fns@2.23.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9" + integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA== + date-fns@2.24.0, date-fns@^2.0.1: version "2.24.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.24.0.tgz#7d86dc0d93c87b76b63d213b4413337cfd1c105d" @@ -12307,6 +12374,18 @@ graphql-helix@1.8.0: resolved "https://registry.yarnpkg.com/graphql-helix/-/graphql-helix-1.8.0.tgz#e3dc18348acb8e792243f808130606c28214c4bd" integrity sha512-tg1tdR2PgCOn2Z/NDSnA9rUR16bLrLtE4el/NwboQ4wWaWM/Eoi3ewV0gtyzKgg61+SjywEac5mDvGgK0AfsDg== +graphql-jit@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/graphql-jit/-/graphql-jit-0.5.2.tgz#f1fe88e106f6e5b6daa43f9f9a021f733697aee6" + integrity sha512-8XNFsdFTUujjqu2q26Vkh4U6ZJ5dKZ2KxtiuCe95VlHK9Xn4+yz1P/nOj7ipZvXhMG4/s2gwoEhkSig9IEoahQ== + dependencies: + fast-json-stringify "^1.13.0" + generate-function "^2.3.1" + json-schema "^0.2.3" + lodash.memoize "^4.1.2" + lodash.merge "4.6.2" + lodash.mergewith "4.6.2" + graphql-jit@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/graphql-jit/-/graphql-jit-0.6.0.tgz#33e7ed3959eca5754b3fbcc44e31a68538b0b028"