diff --git a/CHANGELOG.md b/CHANGELOG.md index 0182085..3d23da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 979f54d..92711d5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index b880dce..86767e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -162,20 +162,26 @@ export default class GraphQLComponent 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): Promise => { diff --git a/test/validation.ts b/test/validation.ts index 07b14b3..354e830 100644 --- a/test/validation.ts +++ b/test/validation.ts @@ -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) => { @@ -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) => 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) => 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(); }); \ No newline at end of file