Skip to content

Commit 7729ab3

Browse files
authored
Merge pull request #114 from tlivings/patch-603
Fix: Support IGraphQLComponent implementations in imports (Node 22 compatibility)
2 parents 7acfd71 + 19dc3b7 commit 7729ab3

File tree

4 files changed

+229
-9
lines changed

4 files changed

+229
-9
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### v6.0.3
2+
3+
- [FIX] Import handling now supports both `GraphQLComponent` instances and custom `IGraphQLComponent` implementations, not just class instances
4+
- [FIX] Changed import normalization from `instanceof GraphQLComponent` check to property-based detection, ensuring all components are properly wrapped
5+
- [TESTS] Added comprehensive tests validating import handling for `GraphQLComponent` instances, `IGraphQLComponent` implementations, and mixed import scenarios
6+
17
### v6.0.2
28

39
- [FIX] DataSources are now available in middleware context, enabling middleware to access component dataSources for authentication and other operations

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-component",
3-
"version": "6.0.2",
3+
"version": "6.0.3",
44
"description": "Build, customize and compose GraphQL schemas in a componentized fashion",
55
"keywords": [
66
"graphql",

src/index.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,20 +162,26 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co
162162

163163
this._pruneSchemaOptions = pruneSchemaOptions;
164164

165-
this._imports = imports && imports.length > 0 ? imports.map((i: GraphQLComponent | IGraphQLComponentConfigObject) => {
166-
if (i instanceof GraphQLComponent) {
167-
if (this._federation === true) {
168-
i.federation = true;
169-
}
170-
return { component: i };
165+
this._imports = imports && imports.length > 0 ? imports.map((i: IGraphQLComponent | IGraphQLComponentConfigObject) => {
166+
if (!i) {
167+
throw new Error('Import cannot be undefined or null');
171168
}
172-
else {
169+
170+
// Check if it's already a config object (has 'component' property)
171+
if ('component' in i && i.component) {
173172
const importConfiguration = i as IGraphQLComponentConfigObject;
174173
if (this._federation === true) {
175174
importConfiguration.component.federation = true;
176175
}
177176
return importConfiguration;
178177
}
178+
179+
// Otherwise, treat it as an IGraphQLComponent and wrap it
180+
const component = i as IGraphQLComponent;
181+
if (this._federation === true) {
182+
component.federation = true;
183+
}
184+
return { component };
179185
}) : [];
180186

181187
this._context = async (globalContext: Record<string, unknown>): Promise<TContextType> => {

test/validation.ts

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import test from 'tape';
2-
import GraphQLComponent from '../src/index';
2+
import GraphQLComponent, { IGraphQLComponent, IGraphQLComponentConfigObject } from '../src/index';
3+
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
34

45
test('GraphQLComponent Configuration Validation', (t) => {
56
t.test('should throw error when federation enabled without types', (assert) => {
@@ -20,5 +21,212 @@ test('GraphQLComponent Configuration Validation', (t) => {
2021
assert.end();
2122
});
2223

24+
t.end();
25+
});
26+
27+
test('GraphQLComponent Import Handling', (t) => {
28+
t.test('should wrap GraphQLComponent instance in config object', (assert) => {
29+
const childComponent = new GraphQLComponent({
30+
types: ['type Query { child: String }']
31+
});
32+
33+
const parentComponent = new GraphQLComponent({
34+
types: ['type Query { parent: String }'],
35+
imports: [childComponent]
36+
});
37+
38+
assert.ok(parentComponent.imports, 'imports array exists');
39+
assert.equals(parentComponent.imports.length, 1, 'has one import');
40+
assert.ok(parentComponent.imports[0].component, 'import has component property');
41+
assert.equals(parentComponent.imports[0].component, childComponent, 'component matches');
42+
assert.end();
43+
});
44+
45+
t.test('should accept IGraphQLComponentConfigObject without wrapping', (assert) => {
46+
const childComponent = new GraphQLComponent({
47+
types: ['type Query { child: String }']
48+
});
49+
50+
const configObject: IGraphQLComponentConfigObject = {
51+
component: childComponent
52+
};
53+
54+
const parentComponent = new GraphQLComponent({
55+
types: ['type Query { parent: String }'],
56+
imports: [configObject]
57+
});
58+
59+
assert.ok(parentComponent.imports, 'imports array exists');
60+
assert.equals(parentComponent.imports.length, 1, 'has one import');
61+
assert.ok(parentComponent.imports[0].component, 'import has component property');
62+
assert.equals(parentComponent.imports[0].component, childComponent, 'component matches');
63+
assert.end();
64+
});
65+
66+
t.test('should wrap custom IGraphQLComponent implementation', (assert) => {
67+
// Create a custom implementation of IGraphQLComponent
68+
const customComponent: IGraphQLComponent = {
69+
get name() {
70+
return 'CustomComponent';
71+
},
72+
get schema() {
73+
return new GraphQLSchema({
74+
query: new GraphQLObjectType({
75+
name: 'Query',
76+
fields: {
77+
custom: {
78+
type: GraphQLString,
79+
resolve: () => 'custom value'
80+
}
81+
}
82+
})
83+
});
84+
},
85+
get context() {
86+
const fn = async (ctx: Record<string, unknown>) => ctx;
87+
fn.use = () => fn;
88+
return fn;
89+
},
90+
get types() {
91+
return ['type Query { custom: String }'];
92+
},
93+
get resolvers() {
94+
return {
95+
Query: {
96+
custom: () => 'custom value'
97+
}
98+
};
99+
},
100+
get imports() {
101+
return undefined;
102+
},
103+
get dataSources() {
104+
return [];
105+
},
106+
get dataSourceOverrides() {
107+
return [];
108+
}
109+
};
110+
111+
const parentComponent = new GraphQLComponent({
112+
types: ['type Query { parent: String }'],
113+
imports: [customComponent]
114+
});
115+
116+
assert.ok(parentComponent.imports, 'imports array exists');
117+
assert.equals(parentComponent.imports.length, 1, 'has one import');
118+
assert.ok(parentComponent.imports[0].component, 'import has component property');
119+
assert.equals(parentComponent.imports[0].component, customComponent, 'component matches');
120+
assert.equals(parentComponent.imports[0].component.name, 'CustomComponent', 'custom component name preserved');
121+
assert.end();
122+
});
123+
124+
t.test('should handle mixed imports (GraphQLComponent and config objects)', (assert) => {
125+
const component1 = new GraphQLComponent({
126+
types: ['type Query { one: String }']
127+
});
128+
129+
const component2 = new GraphQLComponent({
130+
types: ['type Query { two: String }']
131+
});
132+
133+
const configObject: IGraphQLComponentConfigObject = {
134+
component: component2
135+
};
136+
137+
const parentComponent = new GraphQLComponent({
138+
types: ['type Query { parent: String }'],
139+
imports: [component1, configObject]
140+
});
141+
142+
assert.equals(parentComponent.imports.length, 2, 'has two imports');
143+
assert.ok(parentComponent.imports[0].component, 'first import has component');
144+
assert.ok(parentComponent.imports[1].component, 'second import has component');
145+
assert.equals(parentComponent.imports[0].component, component1, 'first component matches');
146+
assert.equals(parentComponent.imports[1].component, component2, 'second component matches');
147+
assert.end();
148+
});
149+
150+
t.test('should set federation flag on imported components when parent has federation', (assert) => {
151+
const childComponent = new GraphQLComponent({
152+
types: ['type Query { child: String }']
153+
});
154+
155+
assert.notOk(childComponent.federation, 'child component federation is false initially');
156+
157+
const parentComponent = new GraphQLComponent({
158+
types: ['type Query { parent: String }'],
159+
imports: [childComponent],
160+
federation: true
161+
});
162+
163+
assert.ok(childComponent.federation, 'child component federation is set to true');
164+
assert.end();
165+
});
166+
167+
t.test('should set federation flag on custom IGraphQLComponent when parent has federation', (assert) => {
168+
let federationFlag = false;
169+
170+
const customComponent: IGraphQLComponent = {
171+
get name() {
172+
return 'CustomComponent';
173+
},
174+
get schema() {
175+
return new GraphQLSchema({
176+
query: new GraphQLObjectType({
177+
name: 'Query',
178+
fields: {
179+
custom: {
180+
type: GraphQLString,
181+
resolve: () => 'custom value'
182+
}
183+
}
184+
})
185+
});
186+
},
187+
get context() {
188+
const fn = async (ctx: Record<string, unknown>) => ctx;
189+
fn.use = () => fn;
190+
return fn;
191+
},
192+
get types() {
193+
return ['type Query { custom: String }'];
194+
},
195+
get resolvers() {
196+
return {
197+
Query: {
198+
custom: () => 'custom value'
199+
}
200+
};
201+
},
202+
get imports() {
203+
return undefined;
204+
},
205+
get dataSources() {
206+
return [];
207+
},
208+
get dataSourceOverrides() {
209+
return [];
210+
},
211+
get federation() {
212+
return federationFlag;
213+
},
214+
set federation(value: boolean) {
215+
federationFlag = value;
216+
}
217+
};
218+
219+
assert.notOk(customComponent.federation, 'custom component federation is false initially');
220+
221+
const parentComponent = new GraphQLComponent({
222+
types: ['type Query { parent: String }'],
223+
imports: [customComponent],
224+
federation: true
225+
});
226+
227+
assert.ok(customComponent.federation, 'custom component federation is set to true');
228+
assert.end();
229+
});
230+
23231
t.end();
24232
});

0 commit comments

Comments
 (0)