Skip to content

Commit

Permalink
initial implementation of transfer schema transform
Browse files Browse the repository at this point in the history
API changes (scope => action, remove rename func, simplify syntax)

finalise functional implementation

update snapshot
  • Loading branch information
santino authored and ardatan committed Apr 4, 2023
1 parent cc4e0a2 commit 8ca6f60
Show file tree
Hide file tree
Showing 11 changed files with 809 additions and 2 deletions.
44 changes: 44 additions & 0 deletions packages/transforms/transfer-schema/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@graphql-mesh/transform-transfer-schema",
"version": "0.1.2",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"exports": {
".": {
"require": {
"types": "./dist/typings/index.d.cts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
},
"default": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./package.json": "./package.json"
},
"typings": "dist/typings/index.d.ts",
"peerDependencies": {
"@graphql-mesh/types": "^0.91.12",
"@graphql-mesh/utils": "^0.43.20",
"graphql": "*"
},
"dependencies": {
"micromatch": "4.0.4"
},
"devDependencies": {
"@types/micromatch": "4.0.2"
},
"publishConfig": {
"access": "public",
"directory": "dist"
},
"sideEffects": false,
"typescript": {
"definition": "dist/typings/index.d.ts"
}
}
138 changes: 138 additions & 0 deletions packages/transforms/transfer-schema/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { GraphQLArgumentConfig, GraphQLFieldConfig, GraphQLObjectType, GraphQLSchema } from 'graphql';
import { MeshTransform, MeshTransformOptions, YamlConfig } from '@graphql-mesh/types';
import { mapSchema, pruneSchema, MapperKind } from '@graphql-tools/utils';
import { matcher } from 'micromatch';

export default class TransferSchemaTransform implements MeshTransform {
noWrap = true;
private additionalFieldsMap: Map<string, Set<string>>;
private additionalArgsMap: Map<string, Set<string>>;
private additionalFieldsConfig: Map<string, { [key: string]: GraphQLFieldConfig<any, any> }>;
private additionalArgsConfig: Map<string, { [key: string]: GraphQLArgumentConfig }>;
private removalFieldsMap: Map<string, Set<string>>;
private removalArgsMap: Map<string, Set<string>>;

constructor(options: MeshTransformOptions<YamlConfig.TransferSchemaTransformConfig>) {
const { config } = options;
this.additionalFieldsMap = new Map();
this.additionalArgsMap = new Map();
this.additionalFieldsConfig = new Map();
this.additionalArgsConfig = new Map();
this.removalFieldsMap = new Map();
this.removalArgsMap = new Map();

for (const transfer of config.transfers) {
const [fromType, fromFieldNameOrGlob, fromArgsGlob] = transfer.from.split('.');

const rawGlob = fromArgsGlob || fromFieldNameOrGlob;
const fixedGlob =
rawGlob.includes('{') && !rawGlob.includes(',') ? rawGlob.replace('{', '').replace('}', '') : rawGlob;
const polishedGlob = fixedGlob.split(', ').join(',').trim();

const mapName = fromArgsGlob ? 'ArgsMap' : 'FieldsMap';
const mapKey = fromArgsGlob ? `${fromType}.${fromFieldNameOrGlob}` : fromType;

if (transfer.action === 'move') {
const currentRemovalRules = this[`removal${mapName}`].get(mapKey) || new Set();
currentRemovalRules.add(polishedGlob);
this[`removal${mapName}`].set(mapKey, currentRemovalRules);
}

const currentAdditionalRules = this[`additional${mapName}`].get(mapKey) || new Set();
currentAdditionalRules.add(`${transfer.to}.${polishedGlob}`);
this[`additional${mapName}`].set(mapKey, currentAdditionalRules);
}
}

handleAdditions(rulesArray: Set<string>, value: string, config: any): void {
for (const rule of rulesArray) {
const [toType, toField, pattern] = rule.split('.');
const usePattern = pattern || toField;
const mapIdentifier = pattern ? 'ArgsConfig' : 'FieldsConfig';
const mapKey = pattern ? `${toType}.${toField}` : toType;

const isMatch = matcher(usePattern);
if (isMatch(value)) {
const currentAdditionalConfigs = this[`additional${mapIdentifier}`].get(mapKey) || {};
this[`additional${mapIdentifier}`].set(mapKey, { ...currentAdditionalConfigs, [value]: config });
break;
}
}
}

matchInSet(rulesSet: Set<string>, value: string): true | undefined {
for (const rule of rulesSet) {
const isMatch = matcher(rule);
if (isMatch(value)) return true;
}
return undefined;
}

transformSchema(schema: GraphQLSchema) {
// initial mapSchema is necessary to store fields/args configs that will be attached to Types/fields in second map
const schemaWithRemovals = mapSchema(schema, {
[MapperKind.COMPOSITE_FIELD]: (fieldConfig, fieldName, typeName) => {
const additionalFieldRules = this.additionalFieldsMap.get(typeName);
const additionalArgRules = this.additionalArgsMap.get(`${typeName}.${fieldName}`);
const removalFieldRules = this.removalFieldsMap.get(typeName);
const removalArgRules = this.removalArgsMap.get(`${typeName}.${fieldName}`);
const hasAdditionalFieldRules = Boolean(additionalFieldRules);
const hasAdditionalArgRules = Boolean(additionalArgRules);
const hasRemovalFieldRules = Boolean(removalFieldRules && removalFieldRules.size);
const hasRemovalArgRules = Boolean(removalArgRules && removalArgRules.size);

// handle field addition
if (hasAdditionalFieldRules) {
this.handleAdditions(additionalFieldRules, fieldName, fieldConfig);
}

// handle args addition
if (hasAdditionalArgRules) {
for (const [argName, argConfig] of Object.entries(fieldConfig.args)) {
this.handleAdditions(additionalArgRules, argName, argConfig);
}
}

// handle field removal
if (hasRemovalFieldRules && this.matchInSet(removalFieldRules, fieldName)) return null;

// handle args removal
if (hasRemovalArgRules) {
const newArgs = Object.entries(fieldConfig.args).reduce((args, [argName, argConfig]) => {
return this.matchInSet(removalArgRules, argName) ? args : { ...args, [argName]: argConfig };
}, {});

return { ...fieldConfig, args: newArgs };
}

return undefined;
},
});

const schemaWithAdditions = mapSchema(schemaWithRemovals, {
...(this.additionalFieldsConfig.size && {
[MapperKind.OBJECT_TYPE]: type => {
const additionalFieldsConfigMap = this.additionalFieldsConfig.get(type.toString());

if (!additionalFieldsConfigMap) return undefined;

const newConfig = { ...type.toConfig() };
newConfig.fields = { ...newConfig.fields, ...additionalFieldsConfigMap };

return new GraphQLObjectType(newConfig);
},
}),
...(this.additionalArgsConfig.size && {
[MapperKind.COMPOSITE_FIELD]: (fieldConfig, fieldName, typeName) => {
const additionalArgsConfigMap = this.additionalArgsConfig.get(`${typeName}.${fieldName}`);

if (!additionalArgsConfigMap) return undefined;

return { ...fieldConfig, args: { ...(fieldConfig.args || {}), ...additionalArgsConfigMap } };
},
}),
});

return pruneSchema(schemaWithAdditions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`transfer-schema transform should copy selected arguments correctly 1`] = `
"type Query {
books(title: String, author: String): [Book]
addBook(title: String, author: String): Book
}
type Mutation {
ourBooks: [Book]
}
type Book {
title: String!
author: String!
}"
`;

exports[`transfer-schema transform should move all fields when using whildcard "*" 1`] = `
"type Query {
books: [Book]
addBook(title: String, author: String): Book
ourBooks: [Book]
}
type Book {
title: String!
author: String!
}"
`;

exports[`transfer-schema transform should move and copy fields correctly 1`] = `
"type Query {
books: [Book]
ourBooks: [Book]
}
type Mutation {
ourBooks: [Book]
addBook(title: String, author: String): Book
}
type Book {
title: String!
author: String!
}"
`;

exports[`transfer-schema transform should move selected arguments correctly 1`] = `
"type Query {
books(title: String, author: String): [Book]
addBook: Book
}
type Mutation {
ourBooks: [Book]
}
type Book {
title: String!
author: String!
}"
`;
Loading

0 comments on commit 8ca6f60

Please sign in to comment.