@graphql-tools/stitching-directives 3.0.0-alpha-be1b2ebd.0 → 3.0.0-alpha-20230517123108-df347e95

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. package/README.md +0 -2
  2. package/cjs/defaultStitchingDirectiveOptions.js +10 -0
  3. package/cjs/extractVariables.js +52 -0
  4. package/cjs/federationToStitchingSDL.js +114 -0
  5. package/cjs/getSourcePaths.js +25 -0
  6. package/cjs/index.js +6 -0
  7. package/cjs/package.json +1 -0
  8. package/cjs/parseMergeArgsExpr.js +69 -0
  9. package/cjs/pathsFromSelectionSet.js +31 -0
  10. package/cjs/preparseMergeArgsExpr.js +31 -0
  11. package/cjs/properties.js +70 -0
  12. package/cjs/stitchingDirectives.js +78 -0
  13. package/cjs/stitchingDirectivesTransformer.js +423 -0
  14. package/cjs/stitchingDirectivesValidator.js +120 -0
  15. package/cjs/types.js +0 -0
  16. package/esm/defaultStitchingDirectiveOptions.js +7 -0
  17. package/esm/extractVariables.js +48 -0
  18. package/esm/federationToStitchingSDL.js +110 -0
  19. package/esm/getSourcePaths.js +21 -0
  20. package/esm/index.js +3 -0
  21. package/esm/parseMergeArgsExpr.js +65 -0
  22. package/esm/pathsFromSelectionSet.js +27 -0
  23. package/esm/preparseMergeArgsExpr.js +27 -0
  24. package/esm/properties.js +63 -0
  25. package/esm/stitchingDirectives.js +74 -0
  26. package/esm/stitchingDirectivesTransformer.js +419 -0
  27. package/esm/stitchingDirectivesValidator.js +116 -0
  28. package/esm/types.js +0 -0
  29. package/package.json +40 -15
  30. package/typings/defaultStitchingDirectiveOptions.d.cts +2 -0
  31. package/{defaultStitchingDirectiveOptions.d.ts → typings/defaultStitchingDirectiveOptions.d.ts} +1 -1
  32. package/typings/extractVariables.d.cts +7 -0
  33. package/{extractVariables.d.ts → typings/extractVariables.d.ts} +1 -1
  34. package/typings/federationToStitchingSDL.d.cts +2 -0
  35. package/{federationToStitchingSDL.d.ts → typings/federationToStitchingSDL.d.ts} +1 -1
  36. package/typings/getSourcePaths.d.cts +3 -0
  37. package/{getSourcePaths.d.ts → typings/getSourcePaths.d.ts} +1 -1
  38. package/typings/index.d.cts +3 -0
  39. package/typings/index.d.ts +3 -0
  40. package/typings/parseMergeArgsExpr.d.cts +3 -0
  41. package/{parseMergeArgsExpr.d.ts → typings/parseMergeArgsExpr.d.ts} +1 -1
  42. package/typings/pathsFromSelectionSet.d.ts +2 -0
  43. package/typings/preparseMergeArgsExpr.d.ts +6 -0
  44. package/typings/properties.d.cts +5 -0
  45. package/{properties.d.ts → typings/properties.d.ts} +1 -1
  46. package/typings/stitchingDirectives.d.cts +19 -0
  47. package/{stitchingDirectives.d.ts → typings/stitchingDirectives.d.ts} +1 -1
  48. package/typings/stitchingDirectivesTransformer.d.cts +3 -0
  49. package/{stitchingDirectivesTransformer.d.ts → typings/stitchingDirectivesTransformer.d.ts} +1 -1
  50. package/typings/stitchingDirectivesValidator.d.cts +3 -0
  51. package/{stitchingDirectivesValidator.d.ts → typings/stitchingDirectivesValidator.d.ts} +1 -1
  52. package/typings/types.d.cts +35 -0
  53. package/{types.d.ts → typings/types.d.ts} +3 -3
  54. package/index.d.ts +0 -3
  55. package/index.js +0 -972
  56. package/index.mjs +0 -967
  57. /package/{pathsFromSelectionSet.d.ts → typings/pathsFromSelectionSet.d.cts} +0 -0
  58. /package/{preparseMergeArgsExpr.d.ts → typings/preparseMergeArgsExpr.d.cts} +0 -0
@@ -0,0 +1,110 @@
1
+ // Taken from https://github.com/gmac/federation-to-stitching-sdl/blob/main/index.js
2
+ import { print, Kind, parse, } from 'graphql';
3
+ import { stitchingDirectives } from './stitchingDirectives.js';
4
+ const extensionKind = /Extension$/;
5
+ const entityKinds = [
6
+ Kind.OBJECT_TYPE_DEFINITION,
7
+ Kind.OBJECT_TYPE_EXTENSION,
8
+ Kind.INTERFACE_TYPE_DEFINITION,
9
+ Kind.INTERFACE_TYPE_EXTENSION,
10
+ ];
11
+ function isEntityKind(def) {
12
+ return entityKinds.includes(def.kind);
13
+ }
14
+ function getQueryTypeDef(definitions) {
15
+ var _a;
16
+ const schemaDef = definitions.find(def => def.kind === Kind.SCHEMA_DEFINITION);
17
+ const typeName = schemaDef
18
+ ? (_a = schemaDef.operationTypes.find(({ operation }) => operation === 'query')) === null || _a === void 0 ? void 0 : _a.type.name.value
19
+ : 'Query';
20
+ return definitions.find(def => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === typeName);
21
+ }
22
+ // Federation services are actually fairly complex,
23
+ // as the `buildFederatedSchema` helper does a fair amount
24
+ // of hidden work to setup the Federation schema specification:
25
+ // https://www.apollographql.com/docs/federation/federation-spec/#federation-schema-specification
26
+ export function federationToStitchingSDL(federationSDL, stitchingConfig = stitchingDirectives()) {
27
+ const doc = parse(federationSDL);
28
+ const entityTypes = [];
29
+ const baseTypeNames = doc.definitions.reduce((memo, typeDef) => {
30
+ if (!extensionKind.test(typeDef.kind) && 'name' in typeDef && typeDef.name) {
31
+ memo[typeDef.name.value] = true;
32
+ }
33
+ return memo;
34
+ }, {});
35
+ doc.definitions.forEach(typeDef => {
36
+ var _a, _b, _c;
37
+ // Un-extend all types (remove "extends" keywords)...
38
+ // extended types are invalid GraphQL without a local base type to extend from.
39
+ // Stitching merges flat types in lieu of hierarchical extensions.
40
+ if (extensionKind.test(typeDef.kind) && 'name' in typeDef && typeDef.name && !baseTypeNames[typeDef.name.value]) {
41
+ typeDef.kind = typeDef.kind.replace(extensionKind, 'Definition');
42
+ }
43
+ if (!isEntityKind(typeDef))
44
+ return;
45
+ // Find object definitions with "@key" directives;
46
+ // these are federated entities that get turned into merged types.
47
+ const keyDirs = [];
48
+ const otherDirs = [];
49
+ (_a = typeDef.directives) === null || _a === void 0 ? void 0 : _a.forEach(dir => {
50
+ if (dir.name.value === 'key') {
51
+ keyDirs.push(dir);
52
+ }
53
+ else {
54
+ otherDirs.push(dir);
55
+ }
56
+ });
57
+ if (!keyDirs.length)
58
+ return;
59
+ // Setup stitching MergedTypeConfig for all federated entities:
60
+ const selectionSet = `{ ${keyDirs.map((dir) => dir.arguments[0].value.value).join(' ')} }`;
61
+ const keyFields = parse(selectionSet).definitions[0].selectionSet.selections.map((sel) => sel.name.value);
62
+ const keyDir = keyDirs[0];
63
+ keyDir.name.value = stitchingConfig.keyDirective.name;
64
+ keyDir.arguments[0].name.value = 'selectionSet';
65
+ keyDir.arguments[0].value.value = selectionSet;
66
+ typeDef.directives = [keyDir, ...otherDirs];
67
+ // Remove non-key "@external" fields from the type...
68
+ // the stitching query planner expects services to only publish their own fields.
69
+ // This makes "@provides" moot because the query planner can automate the logic.
70
+ typeDef.fields = (_b = typeDef.fields) === null || _b === void 0 ? void 0 : _b.filter(fieldDef => {
71
+ var _a;
72
+ return (keyFields.includes(fieldDef.name.value) || !((_a = fieldDef.directives) === null || _a === void 0 ? void 0 : _a.find(dir => dir.name.value === 'external')));
73
+ });
74
+ // Discard remaining "@external" directives and any "@provides" directives
75
+ (_c = typeDef.fields) === null || _c === void 0 ? void 0 : _c.forEach((fieldDef) => {
76
+ fieldDef.directives = fieldDef.directives.filter((dir) => !/^(external|provides)$/.test(dir.name.value));
77
+ fieldDef.directives.forEach((dir) => {
78
+ if (dir.name.value === 'requires') {
79
+ dir.name.value = stitchingConfig.computedDirective.name;
80
+ dir.arguments[0].name.value = 'selectionSet';
81
+ dir.arguments[0].value.value = `{ ${dir.arguments[0].value.value} }`;
82
+ }
83
+ });
84
+ });
85
+ if (typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || typeDef.kind === Kind.OBJECT_TYPE_EXTENSION) {
86
+ entityTypes.push(typeDef.name.value);
87
+ }
88
+ });
89
+ // Federation service SDLs are incomplete because they omit the federation spec itself...
90
+ // (https://www.apollographql.com/docs/federation/federation-spec/#federation-schema-specification)
91
+ // To make federation SDLs into valid and parsable GraphQL schemas,
92
+ // we must fill in the missing details from the specification.
93
+ if (entityTypes.length) {
94
+ const queryDef = getQueryTypeDef(doc.definitions);
95
+ const entitiesSchema = parse(/* GraphQL */ `
96
+ scalar _Any
97
+ union _Entity = ${entityTypes.filter((v, i, a) => a.indexOf(v) === i).join(' | ')}
98
+ type Query { _entities(representations: [_Any!]!): [_Entity]! @${stitchingConfig.mergeDirective.name} }
99
+ `).definitions;
100
+ doc.definitions.push(entitiesSchema[0]);
101
+ doc.definitions.push(entitiesSchema[1]);
102
+ if (queryDef) {
103
+ queryDef.fields.push(entitiesSchema[2].fields[0]);
104
+ }
105
+ else {
106
+ doc.definitions.push(entitiesSchema[2]);
107
+ }
108
+ }
109
+ return [stitchingConfig.stitchingDirectivesTypeDefs, print(doc)].join('\n');
110
+ }
@@ -0,0 +1,21 @@
1
+ import { TypeNameMetaFieldDef } from 'graphql';
2
+ import { pathsFromSelectionSet } from './pathsFromSelectionSet.js';
3
+ export function getSourcePaths(mappingInstructions, selectionSet) {
4
+ const sourcePaths = [];
5
+ for (const mappingInstruction of mappingInstructions) {
6
+ const { sourcePath } = mappingInstruction;
7
+ if (sourcePath.length) {
8
+ sourcePaths.push(sourcePath);
9
+ continue;
10
+ }
11
+ if (selectionSet == null) {
12
+ continue;
13
+ }
14
+ const paths = pathsFromSelectionSet(selectionSet);
15
+ for (const path of paths) {
16
+ sourcePaths.push(path);
17
+ }
18
+ sourcePaths.push([TypeNameMetaFieldDef.name]);
19
+ }
20
+ return sourcePaths;
21
+ }
package/esm/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './stitchingDirectives.js';
2
+ export * from './types.js';
3
+ export * from './federationToStitchingSDL.js';
@@ -0,0 +1,65 @@
1
+ import { parseValue, valueFromASTUntyped } from 'graphql';
2
+ import { extractVariables } from './extractVariables.js';
3
+ import { EXPANSION_PREFIX, KEY_DELIMITER, preparseMergeArgsExpr } from './preparseMergeArgsExpr.js';
4
+ import { propertyTreeFromPaths } from './properties.js';
5
+ import { getSourcePaths } from './getSourcePaths.js';
6
+ export function parseMergeArgsExpr(mergeArgsExpr, selectionSet) {
7
+ const { mergeArgsExpr: newMergeArgsExpr, expansionExpressions } = preparseMergeArgsExpr(mergeArgsExpr);
8
+ const inputValue = parseValue(`{ ${newMergeArgsExpr} }`, { noLocation: true });
9
+ const { inputValue: newInputValue, variablePaths } = extractVariables(inputValue);
10
+ if (!Object.keys(expansionExpressions).length) {
11
+ if (!Object.keys(variablePaths).length) {
12
+ throw new Error('Merge arguments must declare a key.');
13
+ }
14
+ const mappingInstructions = getMappingInstructions(variablePaths);
15
+ const usedProperties = propertyTreeFromPaths(getSourcePaths(mappingInstructions, selectionSet));
16
+ return { args: valueFromASTUntyped(newInputValue), usedProperties, mappingInstructions };
17
+ }
18
+ const expansionRegEx = new RegExp(`^${EXPANSION_PREFIX}[0-9]+$`);
19
+ for (const variableName in variablePaths) {
20
+ if (!variableName.match(expansionRegEx)) {
21
+ throw new Error('Expansions cannot be mixed with single key declarations.');
22
+ }
23
+ }
24
+ const expansions = [];
25
+ const sourcePaths = [];
26
+ for (const variableName in expansionExpressions) {
27
+ const str = expansionExpressions[variableName];
28
+ const valuePath = variablePaths[variableName];
29
+ const { inputValue: expansionInputValue, variablePaths: expansionVariablePaths } = extractVariables(parseValue(`${str}`, { noLocation: true }));
30
+ if (!Object.keys(expansionVariablePaths).length) {
31
+ throw new Error('Merge arguments must declare a key.');
32
+ }
33
+ const mappingInstructions = getMappingInstructions(expansionVariablePaths);
34
+ const value = valueFromASTUntyped(expansionInputValue);
35
+ sourcePaths.push(...getSourcePaths(mappingInstructions, selectionSet));
36
+ assertNotWithinList(valuePath);
37
+ expansions.push({
38
+ valuePath,
39
+ value,
40
+ mappingInstructions,
41
+ });
42
+ }
43
+ const usedProperties = propertyTreeFromPaths(sourcePaths);
44
+ return { args: valueFromASTUntyped(newInputValue), usedProperties, expansions };
45
+ }
46
+ function getMappingInstructions(variablePaths) {
47
+ const mappingInstructions = [];
48
+ for (const keyPath in variablePaths) {
49
+ const valuePath = variablePaths[keyPath];
50
+ const splitKeyPath = keyPath.split(KEY_DELIMITER).slice(1);
51
+ assertNotWithinList(valuePath);
52
+ mappingInstructions.push({
53
+ destinationPath: valuePath,
54
+ sourcePath: splitKeyPath,
55
+ });
56
+ }
57
+ return mappingInstructions;
58
+ }
59
+ function assertNotWithinList(path) {
60
+ for (const pathSegment of path) {
61
+ if (typeof pathSegment === 'number') {
62
+ throw new Error('Insertions cannot be made into a list.');
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,27 @@
1
+ import { Kind } from 'graphql';
2
+ export function pathsFromSelectionSet(selectionSet, path = []) {
3
+ var _a;
4
+ const paths = [];
5
+ for (const selection of selectionSet.selections) {
6
+ const additions = (_a = pathsFromSelection(selection, path)) !== null && _a !== void 0 ? _a : [];
7
+ for (const addition of additions) {
8
+ paths.push(addition);
9
+ }
10
+ }
11
+ return paths;
12
+ }
13
+ function pathsFromSelection(selection, path) {
14
+ var _a, _b;
15
+ if (selection.kind === Kind.FIELD) {
16
+ const responseKey = (_b = (_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : selection.name.value;
17
+ if (selection.selectionSet) {
18
+ return pathsFromSelectionSet(selection.selectionSet, path.concat([responseKey]));
19
+ }
20
+ else {
21
+ return [path.concat([responseKey])];
22
+ }
23
+ }
24
+ else if (selection.kind === Kind.INLINE_FRAGMENT) {
25
+ return pathsFromSelectionSet(selection.selectionSet, path);
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ export const KEY_DELIMITER = '__dot__';
2
+ export const EXPANSION_PREFIX = '__exp';
3
+ export function preparseMergeArgsExpr(mergeArgsExpr) {
4
+ const variableRegex = /\$[_A-Za-z][_A-Za-z0-9.]*/g;
5
+ const dotRegex = /\./g;
6
+ mergeArgsExpr = mergeArgsExpr.replace(variableRegex, variable => variable.replace(dotRegex, KEY_DELIMITER));
7
+ const segments = mergeArgsExpr.split('[[');
8
+ const expansionExpressions = Object.create(null);
9
+ if (segments.length === 1) {
10
+ return { mergeArgsExpr, expansionExpressions };
11
+ }
12
+ let finalSegments = [segments[0]];
13
+ for (let i = 1; i < segments.length; i++) {
14
+ const additionalSegments = segments[i].split(']]');
15
+ if (additionalSegments.length !== 2) {
16
+ throw new Error(`Each opening "[[" must be matched by a closing "]]" without nesting.`);
17
+ }
18
+ finalSegments = finalSegments.concat(additionalSegments);
19
+ }
20
+ let finalMergeArgsExpr = finalSegments[0];
21
+ for (let i = 1; i < finalSegments.length - 1; i += 2) {
22
+ const variableName = `${EXPANSION_PREFIX}${(i - 1) / 2 + 1}`;
23
+ expansionExpressions[variableName] = finalSegments[i];
24
+ finalMergeArgsExpr += `\$${variableName}${finalSegments[i + 1]}`;
25
+ }
26
+ return { mergeArgsExpr: finalMergeArgsExpr, expansionExpressions };
27
+ }
@@ -0,0 +1,63 @@
1
+ export function addProperty(object, path, value) {
2
+ const initialSegment = path[0];
3
+ if (path.length === 1) {
4
+ object[initialSegment] = value;
5
+ return;
6
+ }
7
+ let field = object[initialSegment];
8
+ if (field != null) {
9
+ addProperty(field, path.slice(1), value);
10
+ return;
11
+ }
12
+ if (typeof path[1] === 'string') {
13
+ field = Object.create(null);
14
+ }
15
+ else {
16
+ field = [];
17
+ }
18
+ addProperty(field, path.slice(1), value);
19
+ object[initialSegment] = field;
20
+ }
21
+ export function getProperty(object, path) {
22
+ if (!path.length || object == null) {
23
+ return object;
24
+ }
25
+ const newPath = path.slice();
26
+ const key = newPath.shift();
27
+ if (key == null) {
28
+ return;
29
+ }
30
+ const prop = object[key];
31
+ return getProperty(prop, newPath);
32
+ }
33
+ export function getProperties(object, propertyTree) {
34
+ if (object == null) {
35
+ return object;
36
+ }
37
+ const newObject = Object.create(null);
38
+ for (const key in propertyTree) {
39
+ const subKey = propertyTree[key];
40
+ if (subKey == null) {
41
+ newObject[key] = object[key];
42
+ continue;
43
+ }
44
+ const prop = object[key];
45
+ newObject[key] = deepMap(prop, function deepMapFn(item) {
46
+ return getProperties(item, subKey);
47
+ });
48
+ }
49
+ return newObject;
50
+ }
51
+ export function propertyTreeFromPaths(paths) {
52
+ const propertyTree = Object.create(null);
53
+ for (const path of paths) {
54
+ addProperty(propertyTree, path, null);
55
+ }
56
+ return propertyTree;
57
+ }
58
+ function deepMap(arrayOrItem, fn) {
59
+ if (Array.isArray(arrayOrItem)) {
60
+ return arrayOrItem.map(nestedArrayOrItem => deepMap(nestedArrayOrItem, fn));
61
+ }
62
+ return fn(arrayOrItem);
63
+ }
@@ -0,0 +1,74 @@
1
+ import { GraphQLDirective, GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql';
2
+ import { defaultStitchingDirectiveOptions } from './defaultStitchingDirectiveOptions.js';
3
+ import { stitchingDirectivesValidator } from './stitchingDirectivesValidator.js';
4
+ import { stitchingDirectivesTransformer } from './stitchingDirectivesTransformer.js';
5
+ export function stitchingDirectives(options = {}) {
6
+ const finalOptions = {
7
+ ...defaultStitchingDirectiveOptions,
8
+ ...options,
9
+ };
10
+ const { keyDirectiveName, computedDirectiveName, mergeDirectiveName, canonicalDirectiveName } = finalOptions;
11
+ const keyDirectiveTypeDefs = /* GraphQL */ `directive @${keyDirectiveName}(selectionSet: String!) on OBJECT`;
12
+ const computedDirectiveTypeDefs = /* GraphQL */ `directive @${computedDirectiveName}(selectionSet: String!) on FIELD_DEFINITION`;
13
+ const mergeDirectiveTypeDefs = /* GraphQL */ `directive @${mergeDirectiveName}(argsExpr: String, keyArg: String, keyField: String, key: [String!], additionalArgs: String) on FIELD_DEFINITION`;
14
+ const canonicalDirectiveTypeDefs = /* GraphQL */ `directive @${canonicalDirectiveName} on OBJECT | INTERFACE | INPUT_OBJECT | UNION | ENUM | SCALAR | FIELD_DEFINITION | INPUT_FIELD_DEFINITION`;
15
+ const keyDirective = new GraphQLDirective({
16
+ name: keyDirectiveName,
17
+ locations: ['OBJECT'],
18
+ args: {
19
+ selectionSet: { type: new GraphQLNonNull(GraphQLString) },
20
+ },
21
+ });
22
+ const computedDirective = new GraphQLDirective({
23
+ name: computedDirectiveName,
24
+ locations: ['FIELD_DEFINITION'],
25
+ args: {
26
+ selectionSet: { type: new GraphQLNonNull(GraphQLString) },
27
+ },
28
+ });
29
+ const mergeDirective = new GraphQLDirective({
30
+ name: mergeDirectiveName,
31
+ locations: ['FIELD_DEFINITION'],
32
+ args: {
33
+ argsExpr: { type: GraphQLString },
34
+ keyArg: { type: GraphQLString },
35
+ keyField: { type: GraphQLString },
36
+ key: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) },
37
+ additionalArgs: { type: GraphQLString },
38
+ },
39
+ });
40
+ const canonicalDirective = new GraphQLDirective({
41
+ name: canonicalDirectiveName,
42
+ locations: [
43
+ 'OBJECT',
44
+ 'INTERFACE',
45
+ 'INPUT_OBJECT',
46
+ 'UNION',
47
+ 'ENUM',
48
+ 'SCALAR',
49
+ 'FIELD_DEFINITION',
50
+ 'INPUT_FIELD_DEFINITION',
51
+ ],
52
+ });
53
+ const allStitchingDirectivesTypeDefs = [
54
+ keyDirectiveTypeDefs,
55
+ computedDirectiveTypeDefs,
56
+ mergeDirectiveTypeDefs,
57
+ canonicalDirectiveTypeDefs,
58
+ ].join('\n');
59
+ return {
60
+ keyDirectiveTypeDefs,
61
+ computedDirectiveTypeDefs,
62
+ mergeDirectiveTypeDefs,
63
+ canonicalDirectiveTypeDefs,
64
+ stitchingDirectivesTypeDefs: allStitchingDirectivesTypeDefs,
65
+ allStitchingDirectivesTypeDefs,
66
+ keyDirective,
67
+ computedDirective,
68
+ mergeDirective,
69
+ canonicalDirective,
70
+ allStitchingDirectives: [keyDirective, computedDirective, mergeDirective, canonicalDirective],
71
+ stitchingDirectivesValidator: stitchingDirectivesValidator(finalOptions),
72
+ stitchingDirectivesTransformer: stitchingDirectivesTransformer(finalOptions),
73
+ };
74
+ }