Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### v6.0.3

- [FIX] Import handling now supports both `GraphQLComponent` instances and custom `IGraphQLComponent` implementations, not just class instances
- [FIX] Changed import normalization from `instanceof GraphQLComponent` check to property-based detection, ensuring all components are properly wrapped
- [TESTS] Added comprehensive tests validating import handling for `GraphQLComponent` instances, `IGraphQLComponent` implementations, and mixed import scenarios

### v6.0.2

- [FIX] DataSources are now available in middleware context, enabling middleware to access component dataSources for authentication and other operations
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graphql-component",
"version": "6.0.2",
"version": "6.0.3",
"description": "Build, customize and compose GraphQL schemas in a componentized fashion",
"keywords": [
"graphql",
Expand Down
20 changes: 13 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,26 @@ export default class GraphQLComponent<TContextType extends ComponentContext = Co

this._pruneSchemaOptions = pruneSchemaOptions;

this._imports = imports && imports.length > 0 ? imports.map((i: GraphQLComponent | IGraphQLComponentConfigObject) => {
if (i instanceof GraphQLComponent) {
if (this._federation === true) {
i.federation = true;
}
return { component: i };
this._imports = imports && imports.length > 0 ? imports.map((i: IGraphQLComponent | IGraphQLComponentConfigObject) => {
if (!i) {
throw new Error('Import cannot be undefined or null');
}
else {

// Check if it's already a config object (has 'component' property)
if ('component' in i && i.component) {
const importConfiguration = i as IGraphQLComponentConfigObject;
if (this._federation === true) {
importConfiguration.component.federation = true;
}
return importConfiguration;
}

// Otherwise, treat it as an IGraphQLComponent and wrap it
const component = i as IGraphQLComponent;
if (this._federation === true) {
component.federation = true;
}
return { component };
}) : [];

this._context = async (globalContext: Record<string, unknown>): Promise<TContextType> => {
Expand Down
210 changes: 209 additions & 1 deletion test/validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from 'tape';
import GraphQLComponent from '../src/index';
import GraphQLComponent, { IGraphQLComponent, IGraphQLComponentConfigObject } from '../src/index';
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

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

t.end();
});

test('GraphQLComponent Import Handling', (t) => {
t.test('should wrap GraphQLComponent instance in config object', (assert) => {
const childComponent = new GraphQLComponent({
types: ['type Query { child: String }']
});

const parentComponent = new GraphQLComponent({
types: ['type Query { parent: String }'],
imports: [childComponent]
});

assert.ok(parentComponent.imports, 'imports array exists');
assert.equals(parentComponent.imports.length, 1, 'has one import');
assert.ok(parentComponent.imports[0].component, 'import has component property');
assert.equals(parentComponent.imports[0].component, childComponent, 'component matches');
assert.end();
});

t.test('should accept IGraphQLComponentConfigObject without wrapping', (assert) => {
const childComponent = new GraphQLComponent({
types: ['type Query { child: String }']
});

const configObject: IGraphQLComponentConfigObject = {
component: childComponent
};

const parentComponent = new GraphQLComponent({
types: ['type Query { parent: String }'],
imports: [configObject]
});

assert.ok(parentComponent.imports, 'imports array exists');
assert.equals(parentComponent.imports.length, 1, 'has one import');
assert.ok(parentComponent.imports[0].component, 'import has component property');
assert.equals(parentComponent.imports[0].component, childComponent, 'component matches');
assert.end();
});

t.test('should wrap custom IGraphQLComponent implementation', (assert) => {
// Create a custom implementation of IGraphQLComponent
const customComponent: IGraphQLComponent = {
get name() {
return 'CustomComponent';
},
get schema() {
return new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
custom: {
type: GraphQLString,
resolve: () => 'custom value'
}
}
})
});
},
get context() {
const fn = async (ctx: Record<string, unknown>) => ctx;
fn.use = () => fn;
return fn;
},
get types() {
return ['type Query { custom: String }'];
},
get resolvers() {
return {
Query: {
custom: () => 'custom value'
}
};
},
get imports() {
return undefined;
},
get dataSources() {
return [];
},
get dataSourceOverrides() {
return [];
}
};

const parentComponent = new GraphQLComponent({
types: ['type Query { parent: String }'],
imports: [customComponent]
});

assert.ok(parentComponent.imports, 'imports array exists');
assert.equals(parentComponent.imports.length, 1, 'has one import');
assert.ok(parentComponent.imports[0].component, 'import has component property');
assert.equals(parentComponent.imports[0].component, customComponent, 'component matches');
assert.equals(parentComponent.imports[0].component.name, 'CustomComponent', 'custom component name preserved');
assert.end();
});

t.test('should handle mixed imports (GraphQLComponent and config objects)', (assert) => {
const component1 = new GraphQLComponent({
types: ['type Query { one: String }']
});

const component2 = new GraphQLComponent({
types: ['type Query { two: String }']
});

const configObject: IGraphQLComponentConfigObject = {
component: component2
};

const parentComponent = new GraphQLComponent({
types: ['type Query { parent: String }'],
imports: [component1, configObject]
});

assert.equals(parentComponent.imports.length, 2, 'has two imports');
assert.ok(parentComponent.imports[0].component, 'first import has component');
assert.ok(parentComponent.imports[1].component, 'second import has component');
assert.equals(parentComponent.imports[0].component, component1, 'first component matches');
assert.equals(parentComponent.imports[1].component, component2, 'second component matches');
assert.end();
});

t.test('should set federation flag on imported components when parent has federation', (assert) => {
const childComponent = new GraphQLComponent({
types: ['type Query { child: String }']
});

assert.notOk(childComponent.federation, 'child component federation is false initially');

const parentComponent = new GraphQLComponent({
types: ['type Query { parent: String }'],
imports: [childComponent],
federation: true
});

assert.ok(childComponent.federation, 'child component federation is set to true');
assert.end();
});

t.test('should set federation flag on custom IGraphQLComponent when parent has federation', (assert) => {
let federationFlag = false;

const customComponent: IGraphQLComponent = {
get name() {
return 'CustomComponent';
},
get schema() {
return new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
custom: {
type: GraphQLString,
resolve: () => 'custom value'
}
}
})
});
},
get context() {
const fn = async (ctx: Record<string, unknown>) => ctx;
fn.use = () => fn;
return fn;
},
get types() {
return ['type Query { custom: String }'];
},
get resolvers() {
return {
Query: {
custom: () => 'custom value'
}
};
},
get imports() {
return undefined;
},
get dataSources() {
return [];
},
get dataSourceOverrides() {
return [];
},
get federation() {
return federationFlag;
},
set federation(value: boolean) {
federationFlag = value;
}
};

assert.notOk(customComponent.federation, 'custom component federation is false initially');

const parentComponent = new GraphQLComponent({
types: ['type Query { parent: String }'],
imports: [customComponent],
federation: true
});

assert.ok(customComponent.federation, 'custom component federation is set to true');
assert.end();
});

t.end();
});