Skip to content

Commit 9373e04

Browse files
committed
More tests, fixed transforms.
1 parent 9827246 commit 9373e04

File tree

8 files changed

+439
-182
lines changed

8 files changed

+439
-182
lines changed

dist/index.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ export interface IGraphQLComponent<TContextType extends ComponentContext = Compo
5252
readonly dataSourceOverrides?: IDataSource[];
5353
federation?: boolean;
5454
}
55+
/**
56+
* GraphQLComponent class for building modular GraphQL schemas
57+
* @template TContextType - The type of the context object
58+
* @implements {IGraphQLComponent}
59+
*/
5560
export default class GraphQLComponent<TContextType extends ComponentContext = ComponentContext> implements IGraphQLComponent {
5661
_schema: GraphQLSchema;
5762
_types: TypeSource;
@@ -66,6 +71,7 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
6671
_federation: boolean;
6772
_dataSourceContextInject: DataSourceInjectionFunction;
6873
_transforms: SchemaMapper[];
74+
private _transformedSchema;
6975
constructor({ types, resolvers, mocks, imports, context, dataSources, dataSourceOverrides, pruneSchema, pruneSchemaOptions, federation, transforms }: IGraphQLComponentOptions);
7076
get context(): IContextWrapper;
7177
get name(): string;
@@ -77,4 +83,7 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
7783
get dataSourceOverrides(): IDataSource[];
7884
set federation(flag: boolean);
7985
get federation(): boolean;
86+
dispose(): void;
87+
private transformSchema;
88+
private validateConfig;
8089
}

dist/index.js

Lines changed: 117 additions & 86 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
"scripts": {
1414
"build": "tsc",
1515
"prepublish": "npm run build",
16-
"test": "tape -r ts-node/register test/**.ts",
16+
"test": "tape -r ts-node/register \"test/**/*.ts\"",
1717
"start-composition": "DEBUG=graphql-component ts-node examples/composition/server/index.ts",
1818
"start-federation": "DEBUG=graphql-component node examples/federation/run-federation-example.js",
1919
"lint": "npx eslint src/index.ts",
2020
"cover": "nyc npm test",
21-
"update-deps": "ncu -u && npm install"
21+
"update-deps": "ncu -u && npm install",
22+
"format": "prettier --write \"src/**/*.ts\"",
23+
"precommit": "npm run lint && npm run test",
24+
"prepare": "husky install"
2225
},
2326
"author": "Trevor Livingston <tlivings@gmail.com>",
2427
"repository": "https://github.com/ExpediaGroup/graphql-component",
@@ -31,15 +34,15 @@
3134
"@graphql-tools/schema": "^10.0.8",
3235
"@graphql-tools/stitch": "^9.4.0",
3336
"@graphql-tools/utils": "^10.5.6",
34-
"@types/graphql": "^14.5.0",
35-
"@types/node": "^22.9.1",
3637
"debug": "^4.3.7"
3738
},
3839
"peerDependencies": {
3940
"graphql": "^16.0.0"
4041
},
4142
"devDependencies": {
4243
"@apollo/gateway": "^2.9.3",
44+
"@types/graphql": "^14.5.0",
45+
"@types/node": "^22.9.1",
4346
"@typescript-eslint/eslint-plugin": "^8.22.0",
4447
"@typescript-eslint/parser": "^8.22.0",
4548
"apollo-server": "^3.13.0",
@@ -54,7 +57,9 @@
5457
"tape": "^5.9.0",
5558
"ts-node": "^10.9.2",
5659
"typescript": "^5.6.3",
57-
"typescript-eslint": "^8.22.0"
60+
"typescript-eslint": "^8.22.0",
61+
"husky": "^8.0.0",
62+
"prettier": "^2.8.8"
5863
},
5964
"nyc": {
6065
"include": [
@@ -73,5 +78,8 @@
7378
"sourceMap": true,
7479
"instrument": true,
7580
"all": true
81+
},
82+
"engines": {
83+
"node": ">=18.0.0"
7684
}
7785
}

src/index.ts

Lines changed: 127 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export interface IGraphQLComponent<TContextType extends ComponentContext = Compo
7878
federation?: boolean;
7979
}
8080

81+
/**
82+
* GraphQLComponent class for building modular GraphQL schemas
83+
* @template TContextType - The type of the context object
84+
* @implements {IGraphQLComponent}
85+
*/
8186
export default class GraphQLComponent<TContextType extends ComponentContext = ComponentContext> implements IGraphQLComponent {
8287
_schema: GraphQLSchema;
8388
_types: TypeSource;
@@ -92,6 +97,7 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
9297
_federation: boolean;
9398
_dataSourceContextInject: DataSourceInjectionFunction;
9499
_transforms: SchemaMapper[]
100+
private _transformedSchema: GraphQLSchema;
95101

96102
constructor({
97103
types,
@@ -169,14 +175,18 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
169175
return ctx as TContextType;
170176
};
171177

178+
this.validateConfig({ types, imports, mocks, federation });
179+
172180
}
173181

174182
get context(): IContextWrapper {
175183

176-
const contextFn = async (context): Promise<ComponentContext> => {
184+
const contextFn = async (context: Record<string, unknown>): Promise<ComponentContext> => {
177185
debug(`building root context`);
178-
179-
for (const { name, fn } of contextFn._middleware) {
186+
187+
const middleware: MiddlewareEntry[] = (contextFn as any)._middleware || [];
188+
189+
for (const { name, fn } of middleware) {
180190
debug(`applying ${name} middleware`);
181191
context = await fn(context);
182192
}
@@ -212,74 +222,76 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
212222
}
213223

214224
get schema(): GraphQLSchema {
215-
if (this._schema) {
216-
return this._schema;
217-
}
218-
219-
let makeSchema: (schemaConfig: any) => GraphQLSchema = undefined;
225+
try {
226+
if (this._schema) {
227+
return this._schema;
228+
}
220229

221-
if (this._federation) {
222-
makeSchema = (schemaConfig): GraphQLSchema => {
223-
return buildFederatedSchema(schemaConfig);
224-
};
225-
}
226-
else {
227-
makeSchema = makeExecutableSchema;
228-
}
230+
let makeSchema: (schemaConfig: any) => GraphQLSchema;
229231

230-
if (this._imports.length > 0) {
231-
// iterate through the imports and construct subschema configuration objects
232-
const subschemas = this._imports.map((imp) => {
233-
const { component, configuration = {} } = imp;
232+
if (this._federation) {
233+
makeSchema = buildFederatedSchema;
234+
} else {
235+
makeSchema = makeExecutableSchema;
236+
}
234237

235-
return {
236-
schema: component.schema,
237-
...configuration
238-
};
239-
});
240-
241-
// construct an aggregate schema from the schemas of imported
242-
// components and this component's types/resolvers (if present)
243-
this._schema = stitchSchemas({
244-
subschemas,
245-
typeDefs: this._types,
246-
resolvers: this._resolvers,
247-
mergeDirectives: true
248-
});
249-
}
250-
else {
251-
const schemaConfig = {
252-
typeDefs: mergeTypeDefs(this._types),
253-
resolvers: this._resolvers
238+
if (this._imports.length > 0) {
239+
// iterate through the imports and construct subschema configuration objects
240+
const subschemas = this._imports.map((imp) => {
241+
const { component, configuration = {} } = imp;
242+
243+
return {
244+
schema: component.schema,
245+
...configuration
246+
};
247+
});
248+
249+
// construct an aggregate schema from the schemas of imported
250+
// components and this component's types/resolvers (if present)
251+
this._schema = stitchSchemas({
252+
subschemas,
253+
typeDefs: this._types,
254+
resolvers: this._resolvers,
255+
mergeDirectives: true
256+
});
254257
}
258+
else {
259+
const schemaConfig = {
260+
typeDefs: mergeTypeDefs(this._types),
261+
resolvers: this._resolvers
262+
}
255263

256-
this._schema = makeSchema(schemaConfig);
257-
}
264+
this._schema = makeSchema(schemaConfig);
265+
}
258266

259-
if (this._transforms) {
260-
this._schema = transformSchema(this._schema, this._transforms);
261-
}
267+
if (this._transforms) {
268+
this._schema = this.transformSchema(this._schema, this._transforms);
269+
}
262270

263-
if (this._mocks !== undefined && typeof this._mocks === 'boolean' && this._mocks === true) {
264-
debug(`adding default mocks to the schema for ${this.name}`);
265-
// if mocks are a boolean support simply applying default mocks
266-
this._schema = addMocksToSchema({ schema: this._schema, preserveResolvers: true });
267-
}
268-
else if (this._mocks !== undefined && typeof this._mocks === 'object') {
269-
debug(`adding custom mocks to the schema for ${this.name}`);
270-
// else if mocks is an object, that means the user provided
271-
// custom mocks, with which we pass them to addMocksToSchema so they are applied
272-
this._schema = addMocksToSchema({ schema: this._schema, mocks: this._mocks, preserveResolvers: true });
273-
}
271+
if (this._mocks !== undefined && typeof this._mocks === 'boolean' && this._mocks === true) {
272+
debug(`adding default mocks to the schema for ${this.name}`);
273+
// if mocks are a boolean support simply applying default mocks
274+
this._schema = addMocksToSchema({ schema: this._schema, preserveResolvers: true });
275+
}
276+
else if (this._mocks !== undefined && typeof this._mocks === 'object') {
277+
debug(`adding custom mocks to the schema for ${this.name}`);
278+
// else if mocks is an object, that means the user provided
279+
// custom mocks, with which we pass them to addMocksToSchema so they are applied
280+
this._schema = addMocksToSchema({ schema: this._schema, mocks: this._mocks, preserveResolvers: true });
281+
}
274282

275-
if (this._pruneSchema) {
276-
debug(`pruning the schema for ${this.name}`);
277-
this._schema = pruneSchema(this._schema, this._pruneSchemaOptions);
278-
}
283+
if (this._pruneSchema) {
284+
debug(`pruning the schema for ${this.name}`);
285+
this._schema = pruneSchema(this._schema, this._pruneSchemaOptions);
286+
}
279287

280-
debug(`created schema for ${this.name}`);
288+
debug(`created schema for ${this.name}`);
281289

282-
return this._schema;
290+
return this._schema;
291+
} catch (error) {
292+
debug(`Error creating schema for ${this.name}: ${error}`);
293+
throw new Error(`Failed to create schema for component ${this.name}: ${error.message}`);
294+
}
283295
}
284296

285297
get types(): TypeSource {
@@ -310,6 +322,57 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
310322
return this._federation;
311323
}
312324

325+
public dispose(): void {
326+
this._schema = null;
327+
this._types = null;
328+
this._resolvers = null;
329+
this._imports = null;
330+
this._dataSources = null;
331+
this._dataSourceOverrides = null;
332+
}
333+
334+
private transformSchema(schema: GraphQLSchema, transforms: SchemaMapper[]): GraphQLSchema {
335+
if (this._transformedSchema) {
336+
return this._transformedSchema;
337+
}
338+
339+
const functions = {};
340+
const mapping = {};
341+
342+
for (const transform of transforms) {
343+
for (const [key, fn] of Object.entries(transform)) {
344+
if (!mapping[key]) {
345+
functions[key] = [];
346+
let result = undefined;
347+
mapping[key] = function (...args) {
348+
while (functions[key].length) {
349+
const mapper = functions[key].shift();
350+
result = mapper(...args);
351+
if (!result) {
352+
break;
353+
}
354+
}
355+
return result;
356+
}
357+
}
358+
functions[key].push(fn);
359+
}
360+
}
361+
362+
this._transformedSchema = mapSchema(schema, mapping);
363+
return this._transformedSchema;
364+
}
365+
366+
private validateConfig(options: IGraphQLComponentOptions): void {
367+
if (options.federation && !options.types) {
368+
throw new Error('Federation requires type definitions');
369+
}
370+
371+
if (options.mocks && typeof options.mocks !== 'boolean' && typeof options.mocks !== 'object') {
372+
throw new Error('mocks must be either boolean or object');
373+
}
374+
}
375+
313376
}
314377

315378
/**
@@ -442,34 +505,7 @@ const bindResolvers = function (bindContext: IGraphQLComponent, resolvers: IReso
442505
return boundResolvers;
443506
};
444507

445-
/**
446-
* Transforms a schema using the provided transforms
447-
* @param {GraphQLSchema} schema The schema to transform
448-
* @param {SchemaMapper[]} transforms An array of schema transforms
449-
* @returns {GraphQLSchema} The transformed schema
450-
*/
451-
const transformSchema = function (schema: GraphQLSchema, transforms: SchemaMapper[]) {
452-
const functions = {};
453-
const mapping = {};
454-
455-
for (const transform of transforms) {
456-
for (const [key, fn] of Object.entries(transform)) {
457-
if (!mapping[key]) {
458-
functions[key] = [];
459-
mapping[key] = function (arg) {
460-
while (functions[key].length) {
461-
const mapper = functions[key].shift();
462-
arg = mapper(arg);
463-
if (!arg) {
464-
break;
465-
}
466-
}
467-
return arg;
468-
}
469-
}
470-
functions[key].push(fn);
471-
}
472-
}
473-
474-
return mapSchema(schema, mapping);
508+
interface MiddlewareEntry {
509+
name: string;
510+
fn: ContextFunction;
475511
}

0 commit comments

Comments
 (0)