diff --git a/packages/transforms/transfer-field/package.json b/packages/transforms/transfer-field/package.json new file mode 100644 index 0000000000000..a17690364a321 --- /dev/null +++ b/packages/transforms/transfer-field/package.json @@ -0,0 +1,38 @@ +{ + "name": "@graphql-mesh/transform-transform-field", + "version": "0.0.0", + "sideEffects": false, + "main": "dist/index.js", + "module": "dist/index.mjs", + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + }, + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./*": { + "require": "./dist/*.js", + "import": "./dist/*.mjs" + } + }, + "license": "MIT", + "peerDependencies": { + "graphql": "*" + }, + "dependencies": { + "@graphql-mesh/types": "0.53.0", + "@graphql-tools/utils": "8.5.0" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "devDependencies": { + "@graphql-mesh/cache-inmemory-lru": "^0.5.25", + "@graphql-tools/schema": "8.3.0", + "graphql-subscriptions": "1.2.1" + } +} diff --git a/packages/transforms/transfer-field/src/bareTransferField.ts b/packages/transforms/transfer-field/src/bareTransferField.ts new file mode 100644 index 0000000000000..1b206948518d7 --- /dev/null +++ b/packages/transforms/transfer-field/src/bareTransferField.ts @@ -0,0 +1,90 @@ +import { MeshTransform, MeshTransformOptions, YamlConfig } from '@graphql-mesh/types'; +import { appendObjectFields, pruneSchema, removeObjectFields } from '@graphql-tools/utils'; +import { GraphQLFieldConfigMap, GraphQLSchema } from 'graphql'; + +type FieldConfig = YamlConfig.TransferFieldTransformFieldConfigObject & { + removeObjectFieldsTestFn: (fieldName: string) => boolean; +}; + +export default class TransferFieldTransform implements MeshTransform { + noWrap = true; + + private transfers: FieldConfig[] = []; + + constructor(options: MeshTransformOptions) { + const { config } = options; + + const { + defaults: { + useRegExp: useRegExpDefault = false, + regExpFlags: regExpFlagsDefault = undefined, + action: actionDefault = 'move', + } = {}, + fields: fieldConfigs, + } = config; + + for (const fieldConfig of fieldConfigs) { + const { + from, + to, + useRegExp = useRegExpDefault, + regExpFlags = regExpFlagsDefault, + action = actionDefault, + } = fieldConfig; + + const fieldConfigWithDefaults: FieldConfig = { + from, + to, + useRegExp, + regExpFlags, + action, + removeObjectFieldsTestFn: useRegExp + ? fieldName => new RegExp(from.field, regExpFlags).test(fieldName) + : fieldName => fieldName === from.field, + }; + + this.transfers.push(fieldConfigWithDefaults); + } + } + + transformSchema(schema: GraphQLSchema) { + let transformedSchema = schema; + + for (const fieldConfig of this.transfers) { + const { + from: { type: fromTypeName, field: fromFieldName }, + to: { type: toTypeName, field: toFieldName }, + useRegExp, + regExpFlags, + action, + removeObjectFieldsTestFn, + } = fieldConfig; + + const [schemaWithoutFields, sourceFieldConfigMap] = removeObjectFields( + transformedSchema, + fromTypeName, + removeObjectFieldsTestFn + ); + + if (action === 'move') { + transformedSchema = schemaWithoutFields; + } + + const sourceFieldNames = Object.keys(sourceFieldConfigMap) as string[]; + + const appendFieldConfigMap: GraphQLFieldConfigMap = {}; + for (const sourceFieldName of sourceFieldNames) { + const targetFieldName = useRegExp + ? sourceFieldName.replace(new RegExp(fromFieldName, regExpFlags), toFieldName) + : toFieldName; + + const sourceField = sourceFieldConfigMap[sourceFieldName]; + appendFieldConfigMap[targetFieldName] = sourceField; + } + + transformedSchema = appendObjectFields(transformedSchema, toTypeName, appendFieldConfigMap); + } + + return pruneSchema(transformedSchema); + } +} diff --git a/packages/transforms/transfer-field/src/index.ts b/packages/transforms/transfer-field/src/index.ts new file mode 100644 index 0000000000000..1bd844473ea13 --- /dev/null +++ b/packages/transforms/transfer-field/src/index.ts @@ -0,0 +1,9 @@ +import { MeshTransform, MeshTransformOptions, YamlConfig } from '@graphql-mesh/types'; +import BareTransferField from './bareTransferField'; +import WrapTransferField from './wrapTransferField'; + +export default function HoistFieldTransform( + options: MeshTransformOptions +): MeshTransform { + return options.config.mode === 'bare' ? new BareTransferField(options) : new WrapTransferField(options); +} diff --git a/packages/transforms/transfer-field/src/wrapTransferField.ts b/packages/transforms/transfer-field/src/wrapTransferField.ts new file mode 100644 index 0000000000000..9c48856069c3e --- /dev/null +++ b/packages/transforms/transfer-field/src/wrapTransferField.ts @@ -0,0 +1,9 @@ +import { MeshTransform, MeshTransformOptions, YamlConfig } from '@graphql-mesh/types'; + +export default class WrapTransferField implements MeshTransform { + noWrap = false; + + constructor(options: MeshTransformOptions) { + throw new Error('Not implemented'); + } +} diff --git a/packages/transforms/transfer-field/test/__snapshots__/bareTransferField.spec.ts.snap b/packages/transforms/transfer-field/test/__snapshots__/bareTransferField.spec.ts.snap new file mode 100644 index 0000000000000..fbdab1a49b012 --- /dev/null +++ b/packages/transforms/transfer-field/test/__snapshots__/bareTransferField.spec.ts.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transform-field should copy field to different type - regex 1`] = ` +"type Query { + dummy: String + + \\"\\"\\"Retrieve a list of Books\\"\\"\\" + books(maxResults: Int, orderBy: String): [Book] +} + +type Mutation { + \\"\\"\\"Retrieve a list of Books\\"\\"\\" + books(maxResults: Int, orderBy: String): [Book] +} + +type Book { + title: String! + author: Author! + code: String +} + +type Author { + name: String! + age: Int! +} +" +`; + +exports[`transform-field should move field to different type - no regex 1`] = ` +"type Query { + dummy: String + + \\"\\"\\"Retrieve a list of Books\\"\\"\\" + books(maxResults: Int, orderBy: String): [Book] +} + +type Book { + title: String! + author: Author! + code: String +} + +type Author { + name: String! + age: Int! +} +" +`; + +exports[`transform-field should move field to different type - regex 1`] = ` +"type Query { + dummy: String + + \\"\\"\\"Retrieve a list of Books\\"\\"\\" + books(maxResults: Int, orderBy: String): [Book] +} + +type Book { + title: String! + author: Author! + code: String +} + +type Author { + name: String! + age: Int! +} +" +`; diff --git a/packages/transforms/transfer-field/test/bareTransferField.spec.ts b/packages/transforms/transfer-field/test/bareTransferField.spec.ts new file mode 100644 index 0000000000000..87b4a7daf6c1a --- /dev/null +++ b/packages/transforms/transfer-field/test/bareTransferField.spec.ts @@ -0,0 +1,154 @@ +import { join } from 'path'; +import { execute, parse, printSchema, GraphQLObjectType } from 'graphql'; +import InMemoryLRUCache from '@graphql-mesh/cache-inmemory-lru'; +import { MeshPubSub } from '@graphql-mesh/types'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { PubSub } from 'graphql-subscriptions'; + +import TransferFieldTransform from '../src/bareTransferField'; + +describe('transform-field', () => { + const mockQueryBooks = jest.fn().mockImplementation(() => ({ books: [{ title: 'abc' }, { title: 'def' }] })); + const mockBooksApiResponseBooks = jest.fn().mockImplementation(() => [{ title: 'ghi' }, { title: 'lmn' }]); + + const schemaDefs = /* GraphQL */ ` + type Query { + dummy: String + } + + type Mutation { + """ + Retrieve a list of Books + """ + books(maxResults: Int, orderBy: String): [Book] + } + + type Book { + title: String! + author: Author! + code: String + } + + type Author { + name: String! + age: Int! + } + `; + let cache: InMemoryLRUCache; + let pubsub: MeshPubSub; + const baseDir: string = undefined; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + cache = new InMemoryLRUCache(); + pubsub = new PubSub(); + }); + + it('should move field to different type - no regex', async () => { + const transform = new TransferFieldTransform({ + config: { + mode: 'bare', + fields: [ + { + from: { + type: 'Mutation', + field: 'books', + }, + to: { + type: 'Query', + field: 'books', + }, + }, + ], + }, + cache, + pubsub, + baseDir, + apiName: '', + syncImportFn: require, + }); + const schema = makeExecutableSchema({ + typeDefs: schemaDefs, + }); + const transformedSchema = transform.transformSchema(schema); + + expect(transformedSchema.getType('Mutation')).toBeUndefined(); + expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Book]'); + expect(printSchema(transformedSchema)).toMatchSnapshot(); + }); + + it('should move field to different type - regex', async () => { + const transform = new TransferFieldTransform({ + config: { + mode: 'bare', + fields: [ + { + from: { + type: 'Mutation', + field: '(.*)', + }, + to: { + type: 'Query', + field: '$1', + }, + useRegExp: true, + }, + ], + }, + cache, + pubsub, + baseDir, + apiName: '', + syncImportFn: require, + }); + const schema = makeExecutableSchema({ + typeDefs: schemaDefs, + }); + const transformedSchema = transform.transformSchema(schema); + + expect(transformedSchema.getType('Mutation')).toBeUndefined(); + expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Book]'); + expect(printSchema(transformedSchema)).toMatchSnapshot(); + }); + + it('should copy field to different type - regex', async () => { + const transform = new TransferFieldTransform({ + config: { + mode: 'bare', + fields: [ + { + from: { + type: 'Mutation', + field: '(.*)', + }, + to: { + type: 'Query', + field: '$1', + }, + useRegExp: true, + action: 'copy', + }, + ], + }, + cache, + pubsub, + baseDir, + apiName: '', + syncImportFn: require, + }); + const schema = makeExecutableSchema({ + typeDefs: schemaDefs, + }); + const transformedSchema = transform.transformSchema(schema); + + expect(transformedSchema.getType('Mutation')).toBeDefined(); + expect((transformedSchema.getType('Mutation') as GraphQLObjectType).getFields().books.type.toString()).toBe( + '[Book]' + ); + expect((transformedSchema.getType('Query') as GraphQLObjectType).getFields().books.type.toString()).toBe('[Book]'); + expect(printSchema(transformedSchema)).toMatchSnapshot(); + }); +}); diff --git a/packages/transforms/transfer-field/yaml-config.graphql b/packages/transforms/transfer-field/yaml-config.graphql new file mode 100644 index 0000000000000..5f9524f6d44e1 --- /dev/null +++ b/packages/transforms/transfer-field/yaml-config.graphql @@ -0,0 +1,58 @@ +extend type Transform { + """ + Transformer to replace GraphQL field with partial of full config from a different field + """ + transferField: TransferFieldTransformConfig +} + +type TransferFieldTransformConfig @md { + """ + Specify to apply filter-schema transforms to bare schema or by wrapping original schema + """ + mode: FilterSchemaTransformMode + """ + Array of hoist field configs + """ + fields: [TransferFieldTransformFieldConfigObject!]! + + defaults: TransferFieldTransformConfigDefaults +} + +type TransferFieldTransformConfigDefaults { + action: TransferFieldTransformFieldConfigAction + """ + Use Regular Expression for field names + """ + useRegExp: Boolean + """ + Flags to use in the Regular Expression + """ + regExpFlags: String +} + +type TransferFieldTransformFieldConfigObject @md { + from: TransferConfig! + to: TransferConfig! + """ + Action to perform default is move unless otherwise specified in defaults + """ + action: TransferFieldTransformFieldConfigAction + """ + Use Regular Expression for field names + """ + useRegExp: Boolean + """ + Flags to use in the Regular Expression + """ + regExpFlags: String +} + +type TransferConfig { + type: String! + field: String! +} + +enum TransferFieldTransformFieldConfigAction { + copy + move +} diff --git a/packages/types/src/config-schema.json b/packages/types/src/config-schema.json index 9184c1d4dc2a7..0e03f903170df 100644 --- a/packages/types/src/config-schema.json +++ b/packages/types/src/config-schema.json @@ -438,10 +438,6 @@ } ] }, - "replaceField": { - "$ref": "#/definitions/ReplaceFieldTransformConfig", - "description": "Transformer to replace GraphQL field with partial of full config from a different field" - }, "resolversComposition": { "description": "Transformer to apply composition to resolvers (Any of: ResolversCompositionTransform, Any)", "anyOf": [ @@ -472,6 +468,14 @@ "typeMerging": { "$ref": "#/definitions/TypeMergingConfig", "description": "[Type Merging](https://www.graphql-tools.com/docs/stitch-type-merging) Configuration" + }, + "replaceField": { + "$ref": "#/definitions/ReplaceFieldTransformConfig", + "description": "Transformer to replace GraphQL field with partial of full config from a different field" + }, + "transferField": { + "$ref": "#/definitions/TransferFieldTransformConfig", + "description": "Transformer to replace GraphQL field with partial of full config from a different field" } } }, @@ -2500,89 +2504,6 @@ } } }, - "ReplaceFieldTransformConfig": { - "additionalProperties": false, - "type": "object", - "title": "ReplaceFieldTransformConfig", - "properties": { - "typeDefs": { - "anyOf": [ - { - "type": "object", - "additionalProperties": true - }, - { - "type": "string" - }, - { - "type": "array", - "additionalItems": true - } - ], - "description": "Additional type definition to used to replace field types" - }, - "replacements": { - "type": "array", - "items": { - "$ref": "#/definitions/ReplaceFieldTransformObject" - }, - "additionalItems": false, - "description": "Array of rules to replace fields" - } - }, - "required": ["replacements"] - }, - "ReplaceFieldTransformObject": { - "additionalProperties": false, - "type": "object", - "title": "ReplaceFieldTransformObject", - "properties": { - "from": { - "$ref": "#/definitions/ReplaceFieldConfig" - }, - "to": { - "$ref": "#/definitions/ReplaceFieldConfig" - }, - "scope": { - "type": "string", - "enum": ["config", "hoistValue"], - "description": "Allowed values: config, hoistValue" - }, - "composer": { - "anyOf": [ - { - "type": "object", - "additionalProperties": true - }, - { - "type": "string" - }, - { - "type": "array", - "additionalItems": true - } - ] - }, - "name": { - "type": "string" - } - }, - "required": ["from", "to"] - }, - "ReplaceFieldConfig": { - "additionalProperties": false, - "type": "object", - "title": "ReplaceFieldConfig", - "properties": { - "type": { - "type": "string" - }, - "field": { - "type": "string" - } - }, - "required": ["type", "field"] - }, "ResolversCompositionTransform": { "additionalProperties": false, "type": "object", @@ -2817,6 +2738,174 @@ "description": "Path to the SQL Dump file if you want to build a in-memory database" } } + }, + "ReplaceFieldTransformConfig": { + "additionalProperties": false, + "type": "object", + "title": "ReplaceFieldTransformConfig", + "properties": { + "typeDefs": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "string" + }, + { + "type": "array", + "additionalItems": true + } + ], + "description": "Additional type definition to used to replace field types" + }, + "replacements": { + "type": "array", + "items": { + "$ref": "#/definitions/ReplaceFieldTransformObject" + }, + "additionalItems": false, + "description": "Array of rules to replace fields" + } + }, + "required": ["replacements"] + }, + "ReplaceFieldTransformObject": { + "additionalProperties": false, + "type": "object", + "title": "ReplaceFieldTransformObject", + "properties": { + "from": { + "$ref": "#/definitions/ReplaceFieldConfig" + }, + "to": { + "$ref": "#/definitions/ReplaceFieldConfig" + }, + "scope": { + "type": "string", + "enum": ["config", "hoistValue"], + "description": "Allowed values: config, hoistValue" + }, + "composer": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "string" + }, + { + "type": "array", + "additionalItems": true + } + ] + }, + "name": { + "type": "string" + } + }, + "required": ["from", "to"] + }, + "ReplaceFieldConfig": { + "additionalProperties": false, + "type": "object", + "title": "ReplaceFieldConfig", + "properties": { + "type": { + "type": "string" + }, + "field": { + "type": "string" + } + }, + "required": ["type", "field"] + }, + "TransferFieldTransformConfig": { + "additionalProperties": false, + "type": "object", + "title": "TransferFieldTransformConfig", + "properties": { + "mode": { + "type": "string", + "enum": ["bare", "wrap"], + "description": "Specify to apply filter-schema transforms to bare schema or by wrapping original schema (Allowed values: bare, wrap)" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/TransferFieldTransformFieldConfigObject" + }, + "additionalItems": false, + "description": "Array of hoist field configs" + }, + "defaults": { + "$ref": "#/definitions/TransferFieldTransformConfigDefaults" + } + }, + "required": ["fields"] + }, + "TransferFieldTransformConfigDefaults": { + "additionalProperties": false, + "type": "object", + "title": "TransferFieldTransformConfigDefaults", + "properties": { + "action": { + "type": "string", + "enum": ["copy", "move"], + "description": "Allowed values: copy, move" + }, + "useRegExp": { + "type": "boolean", + "description": "Use Regular Expression for field names" + }, + "regExpFlags": { + "type": "string", + "description": "Flags to use in the Regular Expression" + } + } + }, + "TransferFieldTransformFieldConfigObject": { + "additionalProperties": false, + "type": "object", + "title": "TransferFieldTransformFieldConfigObject", + "properties": { + "from": { + "$ref": "#/definitions/TransferConfig" + }, + "to": { + "$ref": "#/definitions/TransferConfig" + }, + "action": { + "type": "string", + "enum": ["copy", "move"], + "description": "Action to perform default is move unless otherwise specified in defaults (Allowed values: copy, move)" + }, + "useRegExp": { + "type": "boolean", + "description": "Use Regular Expression for field names" + }, + "regExpFlags": { + "type": "string", + "description": "Flags to use in the Regular Expression" + } + }, + "required": ["from", "to"] + }, + "TransferConfig": { + "additionalProperties": false, + "type": "object", + "title": "TransferConfig", + "properties": { + "type": { + "type": "string" + }, + "field": { + "type": "string" + } + }, + "required": ["type", "field"] } }, "title": "Config", diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 25552be269ab2..9076633d9f7ce 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -944,13 +944,14 @@ export interface Transform { * Transformer to rename GraphQL types and fields (Any of: RenameTransform, Any) */ rename?: RenameTransform | any; - replaceField?: ReplaceFieldTransformConfig; /** * Transformer to apply composition to resolvers (Any of: ResolversCompositionTransform, Any) */ resolversComposition?: ResolversCompositionTransform | any; snapshot?: SnapshotTransformConfig; typeMerging?: TypeMergingConfig; + replaceField?: ReplaceFieldTransformConfig; + transferField?: TransferFieldTransformConfig; [k: string]: any; } export interface CacheTransformConfig { @@ -1252,37 +1253,6 @@ export interface RenameConfig1 { type?: string; field?: string; } -/** - * Transformer to replace GraphQL field with partial of full config from a different field - */ -export interface ReplaceFieldTransformConfig { - /** - * Additional type definition to used to replace field types - */ - typeDefs?: any; - /** - * Array of rules to replace fields - */ - replacements: ReplaceFieldTransformObject[]; -} -export interface ReplaceFieldTransformObject { - from: ReplaceFieldConfig; - to: ReplaceFieldConfig1; - /** - * Allowed values: config, hoistValue - */ - scope?: 'config' | 'hoistValue'; - composer?: any; - name?: string; -} -export interface ReplaceFieldConfig { - type: string; - field: string; -} -export interface ReplaceFieldConfig1 { - type: string; - field: string; -} export interface ResolversCompositionTransform { /** * Specify to apply resolvers-composition transforms to bare schema or by wrapping original schema (Allowed values: bare, wrap) @@ -1412,6 +1382,89 @@ export interface MergedRootFieldConfig { */ argsExpr?: string; } +/** + * Transformer to replace GraphQL field with partial of full config from a different field + */ +export interface ReplaceFieldTransformConfig { + /** + * Additional type definition to used to replace field types + */ + typeDefs?: any; + /** + * Array of rules to replace fields + */ + replacements: ReplaceFieldTransformObject[]; +} +export interface ReplaceFieldTransformObject { + from: ReplaceFieldConfig; + to: ReplaceFieldConfig1; + /** + * Allowed values: config, hoistValue + */ + scope?: 'config' | 'hoistValue'; + composer?: any; + name?: string; +} +export interface ReplaceFieldConfig { + type: string; + field: string; +} +export interface ReplaceFieldConfig1 { + type: string; + field: string; +} +/** + * Transformer to replace GraphQL field with partial of full config from a different field + */ +export interface TransferFieldTransformConfig { + /** + * Specify to apply filter-schema transforms to bare schema or by wrapping original schema (Allowed values: bare, wrap) + */ + mode?: 'bare' | 'wrap'; + /** + * Array of hoist field configs + */ + fields: TransferFieldTransformFieldConfigObject[]; + defaults?: TransferFieldTransformConfigDefaults; +} +export interface TransferFieldTransformFieldConfigObject { + from: TransferConfig; + to: TransferConfig1; + /** + * Action to perform default is move unless otherwise specified in defaults (Allowed values: copy, move) + */ + action?: 'copy' | 'move'; + /** + * Use Regular Expression for field names + */ + useRegExp?: boolean; + /** + * Flags to use in the Regular Expression + */ + regExpFlags?: string; +} +export interface TransferConfig { + type: string; + field: string; +} +export interface TransferConfig1 { + type: string; + field: string; +} +export interface TransferFieldTransformConfigDefaults { + /** + * Allowed values: copy, move + */ + action?: 'copy' | 'move'; + /** + * Use Regular Expression for field names + */ + useRegExp?: boolean; + /** + * Flags to use in the Regular Expression + */ + regExpFlags?: string; +} export interface AdditionalStitchingResolverObject { sourceName: string; sourceTypeName: string;