diff --git a/.cursor/rules/components.mdc b/.cursor/rules/components.mdc new file mode 100644 index 0000000..ed03125 --- /dev/null +++ b/.cursor/rules/components.mdc @@ -0,0 +1,179 @@ +# Component Creation Patterns + +## Class Structure + +### Extending GraphQLComponent +- Always extend `GraphQLComponent` class +- Implement constructor with options spread pattern +- Use TypeScript for type safety + +```typescript +import GraphQLComponent from 'graphql-component'; +import { types } from './types'; +import { resolvers } from './resolvers'; +import MyDataSource from './datasource'; + +export default class MyComponent extends GraphQLComponent { + constructor({ dataSources = [new MyDataSource()], ...options } = {}) { + super({ types, resolvers, dataSources, ...options }); + } +} +``` + +### Constructor Pattern +- Default empty object parameter: `= {}` +- Default data sources with spread: `dataSources = [new MyDataSource()]` +- Spread remaining options: `...options` +- Pass all to super: `super({ types, resolvers, dataSources, ...options })` + +### Component References (for Delegation) +- Store component instances as properties when needed for delegation +- Initialize imported components in constructor + +```typescript +export default class ListingComponent extends GraphQLComponent { + propertyComponent: PropertyComponent; + reviewsComponent: ReviewsComponent; + + constructor(options) { + const propertyComponent = new PropertyComponent(); + const reviewsComponent = new ReviewsComponent(); + + super({ + types, + resolvers, + imports: [propertyComponent, reviewsComponent], + ...options + }); + + this.propertyComponent = propertyComponent; + this.reviewsComponent = reviewsComponent; + } +} +``` + +## File Organization + +### Standard Structure +``` +my-component/ +├── index.ts # Component class (default export) +├── types.ts # Schema loader +├── resolvers.ts # Resolver map (named export) +├── schema.graphql # GraphQL SDL +└── datasource.ts # Data source class (default export) +``` + +### Schema Loading Pattern +- Use fs.readFileSync for .graphql files +- Export as named export `types` + +```typescript +// types.ts +import fs from 'fs'; +import path from 'path'; + +export const types = fs.readFileSync( + path.resolve(path.join(__dirname, 'schema.graphql')), + 'utf-8' +); +``` + +### Resolver Export Pattern +- Export as named export `resolvers` +- Use object literal format + +```typescript +// resolvers.ts +export const resolvers = { + Query: { + myField(_, args, context) { + return context.dataSources.MyDataSource.getData(args.id); + } + } +}; +``` + +## Federation vs Composition + +### Composition Components +- Use `imports` to include other components +- Use `delegateToSchema` for cross-component calls +- No federation flag needed + +```typescript +const component = new GraphQLComponent({ + types, + resolvers, + imports: [childComponent1, childComponent2] +}); +``` + +### Federation Components +- Set `federation: true` +- Include federation directives in schema +- Implement `__resolveReference` resolvers + +```typescript +const component = new GraphQLComponent({ + types, + resolvers, + dataSources: [new MyDataSource()], + federation: true // Enable federation +}); +``` + +## Resolver Delegation + +### Cross-Component Calls +- Use `delegateToSchema` from `@graphql-tools/delegate` +- Reference component schema via `this.componentName.schema` +- Pass through context and info + +```typescript +import { delegateToSchema } from '@graphql-tools/delegate'; + +export const resolvers = { + Listing: { + property(root, args, context, info) { + return delegateToSchema({ + schema: this.propertyComponent.schema, + fieldName: 'propertyById', + args: { id: root.id }, + context, + info + }); + } + } +}; +``` + +## Context Usage + +### Accessing Data Sources +- Use destructuring: `{ dataSources }` from context +- Access by class name: `dataSources.MyDataSource` + +```typescript +const resolvers = { + Query: { + user(_, { id }, { dataSources }) { + return dataSources.UserDataSource.getUser(id); + } + } +}; +``` + +### Federation Resolvers +- Include `__resolveReference` for entity resolution +- Use typed parameters for clarity + +```typescript +const resolvers = { + Property: { + __resolveReference(ref: { id: string }, { dataSources }: ComponentContext) { + return dataSources.PropertyDataSource.getPropertyById(ref.id); + } + } +}; +``` diff --git a/.cursor/rules/datasources.mdc b/.cursor/rules/datasources.mdc new file mode 100644 index 0000000..1510587 --- /dev/null +++ b/.cursor/rules/datasources.mdc @@ -0,0 +1,166 @@ +# Data Source Patterns + +## Two Data Access Patterns + +### Pattern 1: Injected Data Sources (Recommended) +- Pass via constructor `dataSources` option +- Access via `context.dataSources.name` +- Automatic context injection via proxy +- Easy testing with `dataSourceOverrides` + +### Pattern 2: Private Data Sources (Alternative) +- Create as component instance properties +- Access via `this.dataSourceName` in resolvers +- Resolvers are bound to component instance +- Manual context passing required +- **Limitation**: No `dataSourceOverrides` support +- **Limitation**: No runtime configuration flexibility + +## Implementation Rules + +### Injected Data Sources +- Always implement `DataSourceDefinition` and `IDataSource` +- Include `name` property (string) for identification +- Context parameter MUST be first in all methods + +```typescript +class MyDataSource implements DataSourceDefinition, IDataSource { + name = 'MyDataSource'; // Required + + // Context MUST be first parameter + async getData(context: ComponentContext, id: string) { + return { id }; + } +} +``` + +### Private Data Sources +- No special interfaces required +- Store as component properties +- Use regular functions (not arrow functions) in resolvers for `this` binding + +```typescript +class MyComponent extends GraphQLComponent { + private myDataSource: MyDataSource; + + constructor(options = {}) { + super({ + resolvers: { + Query: { + // Use regular function for 'this' binding + data(_, { id }, context) { + return this.myDataSource.getData(id, context); + } + } + }, + ...options + }); + + this.myDataSource = new MyDataSource(); + } +} +``` + +### Typing Pattern +- Use generic self-reference: `DataSourceDefinition` +- Import `ComponentContext` from the main library +- Define interfaces for return types when complex + +```typescript +import { DataSourceDefinition, ComponentContext, IDataSource } from 'graphql-component'; + +interface User { + id: string; + name: string; +} + +class UserDataSource implements DataSourceDefinition, IDataSource { + name = 'users'; + + async getUser(context: ComponentContext, id: string): Promise { + // Implementation + } +} +``` + +## Usage in Components + +### Constructor Pattern +- Use default data sources with spread operator +- Allow override through constructor options + +```typescript +export default class MyComponent extends GraphQLComponent { + constructor({ dataSources = [new MyDataSource()], ...options } = {}) { + super({ types, resolvers, dataSources, ...options }); + } +} +``` + +### Resolver Usage +- **NEVER** pass context manually to data source methods +- Context is automatically injected by proxy +- Access via `context.dataSources.DataSourceName` + +```typescript +const resolvers = { + Query: { + user(_, { id }, context) { + // ✅ Correct - context injected automatically + return context.dataSources.users.getUser(id); + + // ❌ Wrong - don't pass context manually + // return context.dataSources.users.getUser(context, id); + } + } +}; +``` + +## Testing Patterns + +### Basic Data Source Testing +```typescript +test('data source injection', async (t) => { + const component = new GraphQLComponent({ + types: `type Query { test: String }`, + dataSources: [new TestDataSource()] + }); + + const context = await component.context({ testValue: 'test' }); + const result = context.dataSources.TestDataSource.getData('arg'); + + t.equal(result, 'expected', 'data source method works'); + t.end(); +}); +``` + +### Override Testing +```typescript +test('data source overrides', async (t) => { + const mockDataSource = new MockDataSource(); + + const component = new GraphQLComponent({ + imports: [originalComponent], + dataSourceOverrides: [mockDataSource] + }); + + const context = await component.context({}); + // Original data source is replaced by mock +}); +``` + +## File Organization + +### Structure +``` +component/ +├── datasource.ts # Data source implementation +├── index.ts # Component class +├── resolvers.ts # Resolver functions +├── schema.graphql # GraphQL schema +└── types.ts # Schema loader +``` + +### Export Pattern +- Default export the data source class +- Keep implementation in separate file from component diff --git a/.cursor/rules/examples.mdc b/.cursor/rules/examples.mdc new file mode 100644 index 0000000..4caecb1 --- /dev/null +++ b/.cursor/rules/examples.mdc @@ -0,0 +1,273 @@ +# Example Creation Patterns + +## Example Structure + +### Directory Organization +- Create examples under `/examples` directory +- Use descriptive folder names (composition, federation, etc.) +- Include working server implementations + +``` +examples/ +├── composition/ # Schema stitching example +│ ├── server/ # Server implementation +│ ├── listing-component/ +│ ├── property-component/ +│ └── reviews-component/ +└── federation/ # Federation example + ├── gateway/ # Federation gateway + ├── property-service/ + └── reviews-service/ +``` + +### Component Structure +- Follow standard component file organization +- Use realistic data and schemas +- Include proper TypeScript types + +``` +example-component/ +├── index.ts # Component class +├── types.ts # Schema loader +├── resolvers.ts # Resolvers with business logic +├── schema.graphql # GraphQL schema definition +└── datasource.ts # Mock/example data source +``` + +## Server Examples + +### Composition Server +- Use ApolloServer for GraphQL endpoint +- Import main component for schema +- Keep configuration minimal but functional + +```typescript +const { ApolloServer } = require('apollo-server'); +const ListingComponent = require('../listing-component'); + +const { schema, context } = new ListingComponent(); + +const server = new ApolloServer({ + schema, + context, + tracing: false +}); + +server.listen().then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); +}); +``` + +### Federation Services +- Create separate services for each domain +- Use distinct ports (4001, 4002, etc.) +- Include federation gateway + +```typescript +// Property service +const run = async function (): Promise => { + const { schema, context } = new PropertyComponent({ + types, + resolvers, + dataSources: [new PropertyDataSource()], + federation: true + }); + + const server = new ApolloServer({ schema, context }); + const { url } = await server.listen({port: 4001}); + console.log(`🚀 Property service ready at ${url}`); +}; +``` + +### Federation Gateway +- Use ApolloGateway for service composition +- List all federated services +- Standard port 4000 for gateway + +```typescript +import { ApolloServer } from 'apollo-server'; +import { ApolloGateway } from '@apollo/gateway'; + +const gateway = new ApolloGateway({ + serviceList: [ + { name: 'property', url: 'http://localhost:4001' }, + { name: 'reviews', url: 'http://localhost:4002' } + ] +}); + +const server = new ApolloServer({ gateway }); +const { url } = await server.listen({port: 4000}); +``` + +## Data Source Examples + +### Mock Data Pattern +- Use in-memory objects for demo data +- Include realistic structure and relationships +- Comment data relationships + +```typescript +// reviews indexed by property id +const reviewsDB: Record = { + 1: [ + { id: 'rev-id-1-a', content: 'this property was great'}, + { id: 'rev-id-1-b', content: 'this property was terrible'} + ], + 2: [ + { id: 'rev-id-2-a', content: 'This property was amazing'}, + { id: 'rev-id-2-b', content: 'I loved the proximity to the beach'} + ] +}; +``` + +### Proper TypeScript Interfaces +- Define interfaces for data structures +- Use in data source implementations +- Export for reuse in resolvers + +```typescript +interface Property { + id: number; + geo: string[]; +} + +interface Review { + id: string; + content: string; +} +``` + +## Schema Examples + +### Composition Schemas +- Show realistic business domains +- Include relationships between components +- Use meaningful field names + +```graphql +# Listing schema - aggregates other components +type Listing { + id: ID! + property: Property # From property component + reviews: [Review] # From reviews component +} + +type Query { + listing(id: ID!): Listing +} +``` + +### Federation Schemas +- Include proper federation directives +- Show entity extensions +- Use @key, @external, @requires properly + +```graphql +# Property service +type Property @key(fields: "id") { + id: ID! + geo: [String] +} + +# Reviews service extending Property +extend type Property @key(fields: "id") { + id: ID! @external + reviews: [Review] @requires(fields: "id") +} +``` + +## Resolver Examples + +### Delegation Patterns +- Show proper use of delegateToSchema +- Include error handling +- Pass context and info correctly + +```typescript +export const resolvers = { + Listing: { + property(root, args, context, info) { + return delegateToSchema({ + schema: this.propertyComponent.schema, + fieldName: 'propertyById', + args: { id: root.id }, + context, + info + }); + } + } +}; +``` + +### Federation Resolvers +- Include __resolveReference implementations +- Show proper typing +- Use context for data source access + +```typescript +const resolvers = { + Property: { + __resolveReference(ref: { id: string }, { dataSources }: ComponentContext) { + return dataSources.PropertyDataSource.getPropertyById(ref.id); + } + } +}; +``` + +## Package Scripts + +### Example Scripts +- Add npm scripts for running examples +- Use DEBUG environment variable +- Include clear naming + +```json +{ + "scripts": { + "start-composition": "DEBUG=graphql-component ts-node examples/composition/server/index.ts", + "start-federation": "DEBUG=graphql-component ts-node examples/federation/run-federation-example.ts" + } +} +``` + +## Documentation + +### Example README Sections +- Include clear instructions to run examples +- Explain what each example demonstrates +- Provide GraphQL playground URLs + +```markdown +## Examples + +### Local Schema Composition +```bash +npm run start-composition +``` +This example shows how to compose multiple GraphQL components into a single schema using schema stitching. + +### Federation Example +```bash +npm run start-federation +``` +This example demonstrates building Apollo Federation subgraphs using GraphQL components. + +Both examples are accessible at `http://localhost:4000/graphql` when running. +``` + +## Best Practices + +### Keep Examples Simple +- Focus on demonstrating specific concepts +- Avoid unnecessary complexity +- Use realistic but minimal data + +### Make Examples Runnable +- Include all necessary dependencies +- Provide clear setup instructions +- Test examples regularly + +### Show Real-World Patterns +- Use meaningful business domains +- Include proper error handling +- Demonstrate best practices diff --git a/.cursor/rules/overview.mdc b/.cursor/rules/overview.mdc new file mode 100644 index 0000000..3147638 --- /dev/null +++ b/.cursor/rules/overview.mdc @@ -0,0 +1,98 @@ +--- +alwaysApply: true +--- + +# Project Overview + +`graphql-component` is a TypeScript library for building modular and composable GraphQL schemas through a component-based architecture. It enables developers to create large-scale GraphQL APIs by composing smaller, focused components that each encapsulate their own schema definitions, resolvers, and data sources. + +The library supports both traditional schema stitching for monolithic applications and Apollo Federation for microservice architectures, making it versatile for different deployment patterns. + +# Key Concepts + +## Component Architecture +- **GraphQLComponent**: Core class that encapsulates schema, resolvers, and data sources +- **Schema Composition**: Components can import other components to build larger schemas +- **Isolation**: Each component manages its own concerns independently + +## Data Source Management +- **Proxy-based Injection**: Automatic context injection into data source methods +- **Override Support**: Ability to replace data sources for testing or different environments +- **Type Safety**: TypeScript interfaces ensure proper data source implementation + +## Schema Construction +- **With Imports**: Creates aggregate schemas by combining imported components +- **Without Imports**: Uses makeExecutableSchema for standalone components +- **Federation Support**: Builds Apollo Federation subgraphs when enabled + +## Context & Middleware +- **Context Middleware**: Chainable middleware for authentication, logging, etc. +- **Namespace Support**: Organized context with component namespaces +- **Global Context**: Shared context across all imported components + +# Tech Stack + +## Core Dependencies +- **TypeScript** - Primary language for type safety and modern JavaScript features +- **GraphQL** - Core GraphQL implementation (peer dependency ^16.0.0) +- **@graphql-tools ecosystem** - Schema manipulation and utilities + - `@graphql-tools/schema` - Schema creation + - `@graphql-tools/stitch` - Schema stitching + - `@graphql-tools/merge` - Type definition merging + - `@graphql-tools/utils` - Common utilities and types + - `@graphql-tools/delegate` - Schema delegation + - `@graphql-tools/mock` - Schema mocking +- **@apollo/federation** - Apollo Federation support for microservices + +## Development Tools +- **tape** - Testing framework for unit tests +- **eslint** - Code linting with TypeScript support +- **prettier** - Code formatting +- **debug** - Runtime debugging utilities +- **ts-node** - TypeScript execution for examples and development + +## Example Dependencies +- **apollo-server** - GraphQL server for examples +- **@apollo/gateway** - Federation gateway for examples + +# Primary Goals + +## Modularity & Composition +- Enable building large GraphQL schemas from smaller, manageable components +- Support both horizontal (feature-based) and vertical (layer-based) composition patterns +- Maintain clear separation of concerns between components + +## Developer Experience +- Provide type-safe APIs with comprehensive TypeScript support +- Offer intuitive patterns for common GraphQL use cases +- Minimize boilerplate while maintaining flexibility + +## Architecture Flexibility +- Support both monolithic and microservice deployment patterns +- Enable gradual migration between architectural approaches +- Provide escape hatches for advanced use cases + +## Production Readiness +- Ensure performance through schema optimization and caching +- Support debugging and monitoring through middleware and logging +- Maintain backward compatibility and clear migration paths + +# Coding Standards + +- 2 spaces for indenting +- space between statements and parenthesis, example: `if (condition)` and `function ()` and `switch ()` +- `else` and `if else` always on new lines +- prefer early returns in if/else rather than many `if else` and `else` conditions +- assign functions to constants instead of just defining functions + +# Contributions + +- Always write tests when introducing new code +- Always keep a changelog +- Update documentation according to changes +- Warn about breaking changes +- Always make the minimal change possible to achieve the requested outcome +- Avoid introducing new dependencies, and ask before you do +- Optimize for code readability and keep comments to a minimum +- Create examples under `/examples` directory to demonstrate complex ideas +- Call out potential performance, security, and resilience issues when you encounter them \ No newline at end of file diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc new file mode 100644 index 0000000..90182ad --- /dev/null +++ b/.cursor/rules/testing.mdc @@ -0,0 +1,306 @@ +# Testing Patterns + +## Test Structure + +### Using Tape Framework +- Use `tape` for all tests (import as `test`) +- Use nested tests with `t.test()` +- Always call `t.plan()` or `t.end()` +- Use descriptive test names + +```typescript +import test from 'tape'; +import GraphQLComponent from '../src'; + +test('component feature tests', (t) => { + t.test('should create basic component', (t) => { + t.plan(1); + + const component = new GraphQLComponent({ types, resolvers }); + t.ok(component.schema, 'schema was created'); + }); + + t.test('should handle data sources', async (t) => { + // async tests use t.end() instead of plan + const context = await component.context({}); + t.ok(context.dataSources, 'data sources injected'); + t.end(); + }); + + t.end(); // End parent test +}); +``` + +## Component Testing + +### Basic Component Creation +```typescript +test('component creation', (t) => { + t.test('should create with types and resolvers', (t) => { + t.plan(2); + + const component = new GraphQLComponent({ + types: `type Query { hello: String }`, + resolvers: { + Query: { + hello: () => 'world' + } + } + }); + + t.ok(component.schema, 'schema created'); + t.ok(component.schema.getQueryType(), 'query type exists'); + }); +}); +``` + +### Schema Execution Testing +```typescript +import { graphql } from 'graphql'; + +test('schema execution', async (t) => { + t.plan(1); + + const component = new GraphQLComponent({ + types: `type Query { hello: String }`, + resolvers: { + Query: { + hello: () => 'world' + } + } + }); + + const result = await graphql({ + schema: component.schema, + source: '{ hello }', + contextValue: {} + }); + + t.equal(result.data?.hello, 'world', 'resolver executed correctly'); +}); +``` + +## Data Source Testing + +### Context Injection Testing +```typescript +test('data source context injection', async (t) => { + t.plan(3); + + class TestDataSource { + name = 'test'; + + getData(context, arg) { + t.ok(context, 'context injected'); + t.equal(context.globalValue, 'test', 'context value passed'); + return `${arg}-result`; + } + } + + const component = new GraphQLComponent({ + types: `type Query { test: String }`, + dataSources: [new TestDataSource()] + }); + + const context = await component.context({ globalValue: 'test' }); + const result = context.dataSources.test.getData('input'); + + t.equal(result, 'input-result', 'data source method worked'); +}); +``` + +### Data Source Override Testing +```typescript +test('data source overrides', async (t) => { + t.plan(1); + + class OriginalDataSource { + name = 'test'; + getData() { return 'original'; } + } + + class MockDataSource { + name = 'test'; + getData() { return 'mock'; } + } + + const component = new GraphQLComponent({ + types: `type Query { test: String }`, + dataSources: [new OriginalDataSource()], + dataSourceOverrides: [new MockDataSource()] + }); + + const context = await component.context({}); + const result = context.dataSources.test.getData(); + + t.equal(result, 'mock', 'override replaced original'); +}); +``` + +## Context Testing + +### Middleware Testing +```typescript +test('context middleware', async (t) => { + t.plan(2); + + const component = new GraphQLComponent({ + types: `type Query { test: String }` + }); + + component.context.use('auth', async (ctx) => ({ + ...ctx, + user: { id: '123' } + })); + + const context = await component.context({ req: {} }); + + t.ok(context.user, 'middleware applied'); + t.equal(context.user.id, '123', 'middleware data correct'); +}); +``` + +### Multiple Middleware Order +```typescript +test('middleware execution order', async (t) => { + t.plan(1); + + const component = new GraphQLComponent({ + types: `type Query { test: String }` + }); + + component.context.use('first', async (ctx) => ({ + ...ctx, + value: 1 + })); + + component.context.use('second', async (ctx) => ({ + ...ctx, + value: (ctx.value as number) + 1 + })); + + const context = await component.context({}); + t.equal(context.value, 2, 'middleware executed in order'); +}); +``` + +## Integration Testing + +### Component Composition Testing +```typescript +test('component composition', async (t) => { + t.plan(2); + + const childComponent = new GraphQLComponent({ + types: `type Query { child: String }`, + resolvers: { + Query: { + child: () => 'child result' + } + } + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + resolvers: { + Query: { + parent: () => 'parent result' + } + }, + imports: [childComponent] + }); + + const result = await graphql({ + schema: parentComponent.schema, + source: '{ parent child }' + }); + + t.equal(result.data?.parent, 'parent result', 'parent resolver works'); + t.equal(result.data?.child, 'child result', 'child resolver works'); +}); +``` + +### Federation Testing +```typescript +test('federation schema creation', (t) => { + t.plan(1); + + const component = new GraphQLComponent({ + types: ` + type User @key(fields: "id") { + id: ID! + name: String + } + `, + resolvers: { + User: { + __resolveReference: (ref) => ({ id: ref.id, name: 'Test User' }) + } + }, + federation: true + }); + + t.ok(component.schema, 'federation schema created'); +}); +``` + +## Mock Testing + +### Default Mocks +```typescript +test('default mocks', async (t) => { + t.plan(1); + + const component = new GraphQLComponent({ + types: `type Query { hello: String }`, + mocks: true + }); + + const result = await graphql({ + schema: component.schema, + source: '{ hello }' + }); + + t.ok(result.data?.hello, 'default mock applied'); +}); +``` + +### Custom Mocks +```typescript +test('custom mocks', async (t) => { + t.plan(1); + + const component = new GraphQLComponent({ + types: `type Query { hello: String }`, + mocks: { + Query: () => ({ + hello: 'Custom mock value' + }) + } + }); + + const result = await graphql({ + schema: component.schema, + source: '{ hello }' + }); + + t.equal(result.data?.hello, 'Custom mock value', 'custom mock applied'); +}); +``` + +## Test Organization + +### File Structure +``` +test/ +├── test.ts # Main component tests +├── context.ts # Context-specific tests +├── datasources.ts # Data source tests +├── schema.ts # Schema transformation tests +└── validation.ts # Validation tests +``` + +### Async Test Pattern +- Use `async (t)` for async tests +- Always call `t.end()` for async tests +- Use `t.plan(n)` for sync tests diff --git a/.gitignore b/.gitignore index 3ea62c7..68743fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,89 +1,87 @@ +# Dependencies +node_modules/ +jspm_packages/ + +# Build outputs +dist/ +build/ +*.tgz + # Logs -logs +logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* +lerna-debug.log* # Runtime data -pids +pids/ *.pid *.seed *.pid.lock -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# Coverage and testing +coverage/ +.nyc_output/ +lib-cov/ -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm +# Environment variables +.env +.env.local +.env.*.local -# Optional eslint cache +# Caching +.npm/ .eslintcache +.yarn-integrity # Optional REPL history .node_repl_history -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next +# Package manager files (consistent with .npmrc setting) +package-lock.json +yarn.lock +pnpm-lock.yaml -# Project files -.project -.idea -.settings -.iml -*.iml -*.sublime-workspace -*.sublime-project +# IDE and editor files .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +.idea/ +*.iml +*.sublime-workspace +*.sublime-project +.project +.settings/ -# Misc -.DS_Store* -ehthumbs.db -Icon? -Thumbs.db -.AppleDouble -.LSOverride +# AI coding assistants +.cursor/ +.copilot/ + +# OS generated files +.DS_Store +.DS_Store? +._* .Spotlight-V100 .Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# Temporary files +*.tmp +*.temp *.swp +*.swo +*~ + +# Next.js (if ever used in examples) +.next/ +out/ -# Package lock -package-lock.json \ No newline at end of file +# Misc +.AppleDouble +.LSOverride \ No newline at end of file diff --git a/.npmignore b/.npmignore index f673119..e314f24 100644 --- a/.npmignore +++ b/.npmignore @@ -1,11 +1,81 @@ +# Source files (only publish compiled dist/) +src/ +*.ts +!*.d.ts + +# Development and testing test/ +tests/ +__tests__/ examples/ -.vscode coverage/ .nyc_output/ -azure-pipelines.yml -.eslintrc -.github -.nycrc + +# Configuration files +tsconfig.json +eslint.config.mjs +.eslintrc* +.prettier* +jest.config.* +.babelrc* +webpack.config.* +rollup.config.* + +# CI/CD and deployment +.github/ +.gitlab-ci.yml +azure-pipelines.yml +.travis.yml +.circleci/ +Dockerfile .dockerignore -__tests__.js \ No newline at end of file +.deployment/ + +# IDE and editor files +.vscode/ +.idea/ +*.iml +*.sublime-workspace +*.sublime-project + +# AI coding assistants +.cursor/ +.copilot/ + +# Package manager files +.npmrc +yarn.lock +pnpm-lock.yaml +package-lock.json + +# Environment and secrets +.env* +.secrets + +# Documentation development +docs/ +*.md +!README.md + +# Logs and temporary files +logs/ +*.log +*.tmp +*.temp +.DS_Store +Thumbs.db + +# Development scripts +scripts/dev/ +scripts/build/ +scripts/test/ + +# Linting and formatting +.eslintcache + +# Miscellaneous +.gitignore +.gitattributes +.editorconfig +.commitlintrc* +.husky/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 857ccfd..b6dcd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,19 @@ +### v6.0.2 + +- [PERFORMANCE] Enhanced context building with parallel import processing, and middleware optimization +- [TESTS] Added comprehensive performance regression tests to validate optimization correctness and prevent breaking changes + +### v6.0.1 + +- Re-release after merge + ### v6.0.0 - Converted to TS - BREAKING removed `delegateToComponent` - BREAKING excludes removed as an option in import configuration. Transforms used instead as part of a `SubschemaConfig`. - BREAKING upgraded to graphql 16.9+ peer +- BREAKING datasource injected context does not contain `datasources` === diff --git a/DATASOURCES.md b/DATASOURCES.md new file mode 100644 index 0000000..1b2d323 --- /dev/null +++ b/DATASOURCES.md @@ -0,0 +1,1266 @@ +# Data Sources Guide + +Data sources in `graphql-component` provide a sophisticated system for managing data access with automatic context injection, type safety, and testing capabilities. This guide covers all aspects of implementing, using, and testing data sources. + +## Table of Contents + +- [Core Concepts](#core-concepts) +- [Implementation](#implementation) +- [TypeScript Integration](#typescript-integration) +- [Context System](#context-system) +- [Testing](#testing) +- [Advanced Patterns](#advanced-patterns) +- [Common Gotchas](#common-gotchas) +- [Migration Guide](#migration-guide) + +## Core Concepts + +### Proxy-Based Context Injection + +`graphql-component` uses a proxy system to automatically inject context into data source methods. This means: + +1. **When implementing** data sources: context is the first parameter +2. **When using** data sources in resolvers: context is automatically injected + +```typescript +// Implementation - context is required as first parameter +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(context: ComponentContext, id: string) { + // Access context for auth, config, etc. + const token = context.auth?.token; + return fetchUser(id, token); + } +} + +// Usage in resolvers - context is automatically injected +const resolvers = { + Query: { + user(_, { id }, context) { + // ✅ Correct - context injected automatically + return context.dataSources.users.getUser(id); + + // ❌ Wrong - don't pass context manually + // return context.dataSources.users.getUser(context, id); + } + } +}; +``` + +### Dual Type System + +The library provides two complementary TypeScript types: + +- **`DataSourceDefinition`**: For implementing data sources (requires context parameter) +- **`DataSource`**: For consuming data sources (context automatically injected) + +This ensures type safety while providing a clean API for both implementation and usage. + +## Data Access Patterns + +There are two distinct patterns for accessing data in `graphql-component`, each with different characteristics and use cases: + +### 1. Injected Data Sources (Recommended) + +This is the primary pattern where data sources are passed via constructor options and accessed through the context object. This pattern provides automatic context injection, testing overrides, and clean separation of concerns. + +### 2. Private Data Sources (Alternative) + +Data sources can also be created as private properties of the component class and accessed via `this` in resolvers. Since resolvers are automatically bound to the component instance, `this` refers to the component. + +## Pattern Comparison + +| Feature | Injected Data Sources | Private Data Sources | +|---------|----------------------|---------------------| +| **Access Method** | `context.dataSources.name` | `this.dataSourceName` | +| **Context Injection** | ✅ Automatic proxy injection | ❌ Manual context passing required | +| **Testing Overrides** | ✅ Via `dataSourceOverrides` | ❌ No built-in override mechanism | +| **Configuration Overrides** | ✅ Runtime data source swapping | ❌ Hardcoded at instantiation | +| **Dependency Injection** | ✅ Constructor injection | ❌ Direct instantiation | +| **Environment Flexibility** | ✅ Easy dev/test/prod variants | ❌ Requires code changes | +| **Type Safety** | ✅ Full TypeScript support | ✅ Standard TypeScript | +| **Resolver Binding** | N/A | ✅ Automatic `this` binding | +| **Use Case** | Data access, external APIs | Component delegation, internal logic | + +## Implementation + +### Pattern 1: Injected Data Sources + +```typescript +import { DataSourceDefinition, ComponentContext, IDataSource } from 'graphql-component'; + +interface User { + id: string; + name: string; + email: string; +} + +class UserDataSource implements DataSourceDefinition, IDataSource { + name = 'users'; // Required for identification + + // Static properties are preserved + private apiUrl = 'https://api.example.com'; + + async getUser(context: ComponentContext, id: string): Promise { + // Access context properties + const { auth, config } = context; + + const response = await fetch(`${this.apiUrl}/users/${id}`, { + headers: { + 'Authorization': `Bearer ${auth?.token}`, + 'X-Request-ID': context.requestId + } + }); + + if (!response.ok) { + return null; + } + + return response.json(); + } + + async getUsersByRole(context: ComponentContext, role: string): Promise { + // Implementation details... + return []; + } + + // Non-function properties are preserved by the proxy + get baseUrl() { + return this.apiUrl; + } +} + +export default UserDataSource; +``` + +### Pattern 2: Private Data Sources + +In this pattern, data sources are created as component properties and accessed via `this` in resolvers. Resolvers are automatically bound to the component instance. + +```typescript +import GraphQLComponent from 'graphql-component'; + +class UserDataSource { + name = 'users'; + private apiUrl = 'https://api.example.com'; + + // Note: No ComponentContext parameter - manual context passing required + async getUser(id: string, context?: any) { + const token = context?.auth?.token; + + const response = await fetch(`${this.apiUrl}/users/${id}`, { + headers: token ? { 'Authorization': `Bearer ${token}` } : {} + }); + + return response.json(); + } +} + +class UserComponent extends GraphQLComponent { + private userDataSource: UserDataSource; + + constructor(options = {}) { + super({ + types: ` + type User { id: ID!, name: String! } + type Query { user(id: ID!): User } + `, + resolvers: { + Query: { + // 'this' is automatically bound to the component instance + user(_, { id }, context) { + // Access private data source via 'this' + return this.userDataSource.getUser(id, context); + } + } + }, + ...options + }); + + // Create data source as private property + this.userDataSource = new UserDataSource(); + } +} + +export default UserComponent; +``` + +### Pattern 2 Limitations + +**Important**: Private data sources sacrifice flexibility for direct control. Key limitations include: + +#### No Configuration-Based Overrides +```typescript +// ❌ This won't work with private data sources +const component = new UserComponent({ + dataSourceOverrides: [new MockUserDataSource()] // Only works with injected pattern +}); + +// ❌ Private data sources are hardcoded +class UserComponent extends GraphQLComponent { + constructor(options = {}) { + // Data source is created here and can't be overridden via options + this.userDataSource = new UserDataSource(); + } +} +``` + +#### No Environment-Based Swapping +```typescript +// ✅ Easy with injected pattern +const dataSources = process.env.NODE_ENV === 'test' + ? [new MockUserDataSource()] + : [new ProdUserDataSource()]; + +const component = new GraphQLComponent({ + dataSources, + // ... +}); + +// ❌ Harder with private pattern - requires conditional logic in constructor +class UserComponent extends GraphQLComponent { + constructor(options = {}) { + super(options); + + // Must handle environment logic manually + this.userDataSource = process.env.NODE_ENV === 'test' + ? new MockUserDataSource() + : new ProdUserDataSource(); + } +} +``` + +#### Limited Testing Flexibility +```typescript +// ✅ Injected pattern - easy testing +test('with injected data sources', async (t) => { + const component = new GraphQLComponent({ + types, + resolvers, + dataSourceOverrides: [new MockDataSource()] // Simple override + }); +}); + +// ❌ Private pattern - requires class extension or dependency injection design +test('with private data sources', async (t) => { + // Must extend the class or redesign for injection + class TestComponent extends UserComponent { + constructor() { + super(); + this.userDataSource = new MockDataSource(); // Override after construction + } + } + + const component = new TestComponent(); +}); +``` + +#### No Runtime Reconfiguration +```typescript +// ✅ Injected pattern supports runtime changes +const component = new GraphQLComponent({ + dataSources: [new UserDataSource()] +}); + +// Later, create new instance with different data sources +const testComponent = new GraphQLComponent({ + imports: [component], + dataSourceOverrides: [new TestDataSource()] +}); + +// ❌ Private pattern is fixed at construction time +class UserComponent extends GraphQLComponent { + constructor() { + // Once set, this.userDataSource cannot be changed via configuration + this.userDataSource = new UserDataSource(); + } +} +``` + +**When to Accept These Limitations**: Use private data sources when you need direct control and these limitations are acceptable, such as: +- Component delegation (accessing other component schemas) +- Component-specific configuration that doesn't need to change +- Internal component logic that shouldn't be externally configurable + +### Pattern 2 Use Cases + +Private data sources are particularly useful for: + +**Component Delegation**: Accessing other component schemas for cross-component calls: + +```typescript +import PropertyComponent from './property-component'; +import ReviewsComponent from './reviews-component'; + +class ListingComponent extends GraphQLComponent { + propertyComponent: PropertyComponent; + reviewsComponent: ReviewsComponent; + + constructor(options = {}) { + const propertyComponent = new PropertyComponent(); + const reviewsComponent = new ReviewsComponent(); + + super({ + types: ` + type Listing { + id: ID! + property: Property + reviews: [Review] + } + `, + resolvers: { + Listing: { + // Use 'this' to access component references + property(root, args, context, info) { + return delegateToSchema({ + schema: this.propertyComponent.schema, + fieldName: 'propertyById', + args: { id: root.id }, + context, + info + }); + }, + + reviews(root, args, context, info) { + return delegateToSchema({ + schema: this.reviewsComponent.schema, + fieldName: 'reviewsByPropertyId', + args: { propertyId: root.id }, + context, + info + }); + } + } + }, + imports: [propertyComponent, reviewsComponent], + ...options + }); + + // Store component references for delegation + this.propertyComponent = propertyComponent; + this.reviewsComponent = reviewsComponent; + } +} +``` + +**Component State**: Storing component-specific configuration or state: + +```typescript +class UserComponent extends GraphQLComponent { + private config: { timeout: number; retries: number }; + + constructor({ timeout = 5000, retries = 3, ...options } = {}) { + super({ + resolvers: { + Query: { + user(_, { id }, context) { + // Access component configuration via 'this' + return this.fetchUserWithConfig(id, context); + } + } + }, + ...options + }); + + this.config = { timeout, retries }; + } + + private async fetchUserWithConfig(id: string, context: any) { + // Use component-specific configuration + const { timeout, retries } = this.config; + // Implementation with timeout and retry logic + } +} +``` + +### Advanced Implementation with Interfaces + +For complex data sources, define explicit interfaces: + +```typescript +interface UserDataSourceInterface { + getUser: (context: ComponentContext, id: string) => Promise; + getUsersByRole: (context: ComponentContext, role: string) => Promise; + getUsersByTeam: (context: ComponentContext, teamId: string) => Promise; + cacheTimeout: number; +} + +class UserDataSource implements DataSourceDefinition, IDataSource { + name = 'users'; + cacheTimeout = 300; // 5 minutes + + async getUser(context: ComponentContext, id: string): Promise { + // Implementation + } + + async getUsersByRole(context: ComponentContext, role: string): Promise { + // Implementation + } + + async getUsersByTeam(context: ComponentContext, teamId: string): Promise { + // Implementation + } +} +``` + +## TypeScript Integration + +### Type Safety in Components + +```typescript +import GraphQLComponent from 'graphql-component'; +import UserDataSource from './datasource'; + +export default class UserComponent extends GraphQLComponent { + constructor({ dataSources = [new UserDataSource()], ...options } = {}) { + super({ + types, + resolvers, + dataSources, + ...options + }); + } +} +``` + +### Type Safety in Resolvers + +```typescript +import { ComponentContext } from 'graphql-component'; + +const resolvers = { + Query: { + // Destructure dataSources with types + user(_: any, { id }: { id: string }, { dataSources }: ComponentContext) { + return dataSources.users.getUser(id); + }, + + // Or use full context with explicit typing + usersByRole(_: any, { role }: { role: string }, context: ComponentContext) { + return context.dataSources.users.getUsersByRole(role); + } + }, + + User: { + // Access other data sources from the same context + team(user: User, _: any, { dataSources }: ComponentContext) { + return dataSources.teams.getTeam(user.teamId); + } + } +}; +``` + +### Advanced TypeScript Patterns + +For complex applications, you can extend the `ComponentContext` interface: + +```typescript +// types/context.ts +declare module 'graphql-component' { + interface ComponentContext { + auth: { + token: string; + userId: string; + roles: string[]; + }; + requestId: string; + config: { + apiUrl: string; + timeout: number; + }; + } +} +``` + +## Context System + +### Context Structure + +The context passed to data sources includes: + +```typescript +interface ComponentContext { + dataSources: DataSourceMap; + // Additional context from middleware and component configuration + [key: string]: unknown; +} +``` + +**Important**: The context injected into data source methods does **NOT** include the `dataSources` property. This is an intentional design decision to prevent data sources from calling other data sources directly. + +```typescript +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(context: ComponentContext, id: string) { + // ✅ Available: auth, config, request data, etc. + const token = context.auth?.token; + const requestId = context.requestId; + + // ❌ NOT available: context.dataSources is undefined here + // This prevents: context.dataSources.teams.getTeam(user.teamId) + + return fetchUser(id, token); + } +} +``` + +This architectural constraint serves several important purposes: + +1. **Prevents Tight Coupling**: Data sources remain independent and reusable +2. **Avoids Circular Dependencies**: Eliminates risk of data sources calling each other in loops +3. **Separation of Concerns**: Data fetching stays in data sources, composition happens in resolvers +4. **Better Testing**: Each data source can be tested in isolation +5. **Clearer Architecture**: Forces explicit data composition patterns in resolvers + +### Accessing Context Data + +```typescript +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(context: ComponentContext, id: string) { + // Access authentication + const token = context.auth?.token; + + // Access request metadata + const requestId = context.requestId; + + // Access configuration + const timeout = context.config?.timeout || 5000; + + // Access other context data + const customData = context.customNamespace?.data; + + return fetchUserWithAuth(id, token, { requestId, timeout }); + } +} +``` + +### Data Composition Patterns + +Since data sources cannot call other data sources, data composition must happen in resolvers. This leads to cleaner, more maintainable code: + +```typescript +// ✅ Correct: Compose data in resolvers +const resolvers = { + User: { + // Fetch user's team information + async team(user, _, { dataSources }) { + return dataSources.teams.getTeam(user.teamId); + }, + + // Fetch user's recent activities + async recentActivities(user, _, { dataSources }) { + return dataSources.activities.getActivitiesByUser(user.id); + } + }, + + Query: { + // Compose user with related data + async userWithDetails(_, { id }, { dataSources }) { + const user = await dataSources.users.getUser(id); + if (!user) return null; + + // GraphQL will automatically resolve the team and recentActivities fields + // using the resolvers above when requested + return user; + } + } +}; + +// ❌ Wrong: Don't try to compose in data sources +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUserWithTeam(context: ComponentContext, id: string) { + const user = await this.getUser(context, id); + + // This won't work - context.dataSources is undefined + // const team = await context.dataSources.teams.getTeam(user.teamId); + + return user; + } +} +``` + +This pattern provides several benefits: + +- **Flexible Queries**: Clients can request only the data they need +- **Parallel Execution**: GraphQL can fetch related data in parallel when possible +- **Cacheable**: Each data source method can be cached independently +- **Testable**: Each resolver and data source can be tested in isolation +- **Reusable**: Data sources remain focused and reusable across different contexts + +### Context Middleware + +Data sources work seamlessly with context middleware: + +```typescript +const component = new GraphQLComponent({ + types, + resolvers, + dataSources: [new UserDataSource()] +}); + +// Add authentication middleware +component.context.use('auth', async (context) => { + const token = extractTokenFromRequest(context.req); + const user = await validateToken(token); + + return { + ...context, + auth: { + token, + userId: user.id, + roles: user.roles + } + }; +}); + +// Add request tracking +component.context.use('tracking', async (context) => { + return { + ...context, + requestId: generateRequestId() + }; +}); +``` + +## Testing + +### Basic Data Source Testing + +```typescript +import test from 'tape'; +import GraphQLComponent from 'graphql-component'; +import UserDataSource from '../src/datasource'; + +test('UserDataSource', (t) => { + t.test('should inject context correctly', async (assert) => { + const component = new GraphQLComponent({ + types: `type Query { test: String }`, + dataSources: [new UserDataSource()] + }); + + const context = await component.context({ + auth: { token: 'test-token' }, + requestId: 'test-123' + }); + + // Context is automatically injected + const user = await context.dataSources.users.getUser('user-1'); + + assert.ok(user, 'user was retrieved'); + assert.end(); + }); +}); +``` + +### Mock Data Sources for Testing + +```typescript +class MockUserDataSource implements DataSourceDefinition, IDataSource { + name = 'users'; + + private mockUsers = new Map([ + ['1', { id: '1', name: 'John Doe', email: 'john@example.com' }], + ['2', { id: '2', name: 'Jane Smith', email: 'jane@example.com' }] + ]); + + async getUser(context: ComponentContext, id: string) { + // Mock implementation with context access + console.log(`Mock: Getting user ${id} for request ${context.requestId}`); + return this.mockUsers.get(id) || null; + } + + async getUsersByRole(context: ComponentContext, role: string) { + // Return filtered mock data + return Array.from(this.mockUsers.values()) + .filter(user => user.role === role); + } +} + +// Use in tests +test('component with mock data source', async (t) => { + const component = new GraphQLComponent({ + types, + resolvers, + dataSourceOverrides: [new MockUserDataSource()] // Override real data source + }); + + const context = await component.context({ requestId: 'test-123' }); + const user = await context.dataSources.users.getUser('1'); + + t.equal(user.name, 'John Doe', 'mock data source returned expected user'); + t.end(); +}); +``` + +### Testing Private Data Sources + +**Important**: Private data sources **cannot** use `dataSourceOverrides` for testing. This is a key limitation that requires alternative testing strategies: + +```typescript +test('private data source testing', async (t) => { + t.test('test by extending component class', async (assert) => { + class MockUserDataSource { + name = 'users'; + async getUser(id: string, context?: any) { + return { id, name: 'Mock User', email: 'mock@example.com' }; + } + } + + // Create test component by extending original + class TestUserComponent extends UserComponent { + constructor(options = {}) { + super(options); + // Override the private data source + this.userDataSource = new MockUserDataSource(); + } + } + + const component = new TestUserComponent(); + const context = await component.context({}); + + // Test via GraphQL execution + const result = await graphql({ + schema: component.schema, + source: '{ user(id: "1") { name } }', + contextValue: context + }); + + assert.equal(result.data?.user.name, 'Mock User', 'private data source was mocked'); + assert.end(); + }); + + t.test('test by dependency injection', async (assert) => { + // Design component to accept data source via constructor + class ConfigurableUserComponent extends GraphQLComponent { + private userDataSource: any; + + constructor({ userDataSource = new UserDataSource(), ...options } = {}) { + super({ + resolvers: { + Query: { + user(_, { id }, context) { + return this.userDataSource.getUser(id, context); + } + } + }, + ...options + }); + + this.userDataSource = userDataSource; + } + } + + // Inject mock data source + const mockDataSource = { + name: 'users', + async getUser(id: string) { + return { id, name: 'Injected Mock User' }; + } + }; + + const component = new ConfigurableUserComponent({ + userDataSource: mockDataSource + }); + + const context = await component.context({}); + const result = await graphql({ + schema: component.schema, + source: '{ user(id: "1") { name } }', + contextValue: context + }); + + assert.equal(result.data?.user.name, 'Injected Mock User', 'injected mock data source works'); + assert.end(); + }); +}); +``` + +### Testing with Component Imports + +```typescript +test('data source overrides with imports', async (t) => { + const userComponent = new UserComponent(); + + const testComponent = new GraphQLComponent({ + imports: [userComponent], + dataSourceOverrides: [new MockUserDataSource()] + }); + + const context = await testComponent.context({}); + + // Even though UserComponent has its own UserDataSource, + // the override replaces it + const user = await context.dataSources.users.getUser('1'); + t.equal(user.name, 'John Doe', 'override replaced original data source'); + t.end(); +}); +``` + +### Integration Testing with GraphQL + +```typescript +import { graphql } from 'graphql'; + +test('full integration with resolvers', async (t) => { + const component = new GraphQLComponent({ + types: ` + type User { + id: ID! + name: String! + email: String! + } + type Query { + user(id: ID!): User + } + `, + resolvers: { + Query: { + user(_, { id }, { dataSources }) { + return dataSources.users.getUser(id); + } + } + }, + dataSourceOverrides: [new MockUserDataSource()] + }); + + const context = await component.context({ requestId: 'test-456' }); + + const result = await graphql({ + schema: component.schema, + source: ` + query { + user(id: "1") { + id + name + email + } + } + `, + contextValue: context + }); + + t.ok(result.data?.user, 'user query returned data'); + t.equal(result.data.user.name, 'John Doe', 'correct user data returned'); + t.end(); +}); +``` + +## Advanced Patterns + +### Caching Data Sources + +```typescript +class CachedUserDataSource implements DataSourceDefinition { + name = 'users'; + private cache = new Map(); + private cacheTimeout = 5 * 60 * 1000; // 5 minutes + + async getUser(context: ComponentContext, id: string) { + const cacheKey = `user:${id}`; + const cached = this.cache.get(cacheKey); + + if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { + return cached.data; + } + + const user = await this.fetchUser(context, id); + + this.cache.set(cacheKey, { + data: user, + timestamp: Date.now() + }); + + return user; + } + + private async fetchUser(context: ComponentContext, id: string) { + // Actual data fetching logic + return { id, name: `User ${id}` }; + } +} +``` + +### Batch Loading + +```typescript +import DataLoader from 'dataloader'; + +class BatchUserDataSource implements DataSourceDefinition { + name = 'users'; + private loader: DataLoader; + + constructor() { + this.loader = new DataLoader(async (ids: readonly string[]) => { + // Batch fetch all users + const users = await this.batchFetchUsers([...ids]); + + // Return in same order as input + return ids.map(id => users.find(user => user.id === id) || null); + }); + } + + async getUser(context: ComponentContext, id: string) { + // Use DataLoader for automatic batching + return this.loader.load(id); + } + + private async batchFetchUsers(ids: string[]): Promise { + // Implement batch fetching logic + return []; + } +} +``` + +### Environment-Specific Data Sources + +```typescript +// Production data source +class ProdUserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(context: ComponentContext, id: string) { + // Real API call + return fetch(`/api/users/${id}`).then(r => r.json()); + } +} + +// Development data source +class DevUserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(context: ComponentContext, id: string) { + // Mock data for development + return { id, name: `Dev User ${id}`, email: `user${id}@dev.local` }; + } +} + +// Component with environment-specific defaults +export default class UserComponent extends GraphQLComponent { + constructor(options = {}) { + const defaultDataSource = process.env.NODE_ENV === 'production' + ? new ProdUserDataSource() + : new DevUserDataSource(); + + super({ + types, + resolvers, + dataSources: [defaultDataSource], + ...options + }); + } +} +``` + +## Common Gotchas + +### ❌ Trying to Access Other Data Sources + +```typescript +// Wrong - context.dataSources is undefined in data source methods +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUserWithTeam(context: ComponentContext, id: string) { + const user = await this.getUser(context, id); + + // ❌ This will throw an error - dataSources is not available + const team = await context.dataSources.teams.getTeam(user.teamId); + + return { ...user, team }; + } +} + +// Correct - compose data in resolvers +const resolvers = { + User: { + async team(user, _, { dataSources }) { + // ✅ dataSources is available in resolvers + return dataSources.teams.getTeam(user.teamId); + } + } +}; +``` + +**Why this restriction exists**: Prevents tight coupling, circular dependencies, and maintains clean separation of concerns. + +### ❌ Passing Context Manually + +```typescript +// Wrong - context is injected automatically +const resolvers = { + Query: { + user(_, { id }, context) { + return context.dataSources.users.getUser(context, id); // ❌ Don't do this + } + } +}; + +// Correct - context injection is automatic +const resolvers = { + Query: { + user(_, { id }, context) { + return context.dataSources.users.getUser(id); // ✅ Correct + } + } +}; +``` + +### ❌ Incorrect Interface Implementation + +```typescript +// Wrong - missing context parameter +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(id: string) { // ❌ Missing context parameter + return { id }; + } +} + +// Correct - context as first parameter +class UserDataSource implements DataSourceDefinition { + name = 'users'; + + async getUser(context: ComponentContext, id: string) { // ✅ Context first + return { id }; + } +} +``` + +### ❌ Missing Name Property + +```typescript +// Wrong - no name property +class UserDataSource implements DataSourceDefinition { + async getUser(context: ComponentContext, id: string) { + return { id }; + } +} + +// Correct - include name property +class UserDataSource implements DataSourceDefinition { + name = 'users'; // ✅ Required for identification + + async getUser(context: ComponentContext, id: string) { + return { id }; + } +} +``` + +### ❌ Binding Issues with Arrow Functions + +```typescript +// Potentially problematic - arrow functions don't bind properly +class UserDataSource implements DataSourceDefinition { + name = 'users'; + private apiUrl = 'https://api.example.com'; + + getUser = async (context: ComponentContext, id: string) => { + // This might not have correct 'this' binding in some cases + return fetch(`${this.apiUrl}/users/${id}`); + } +} + +// Preferred - regular methods +class UserDataSource implements DataSourceDefinition { + name = 'users'; + private apiUrl = 'https://api.example.com'; + + async getUser(context: ComponentContext, id: string) { + // Correct 'this' binding guaranteed + return fetch(`${this.apiUrl}/users/${id}`); + } +} +``` + +### ❌ Forgetting Resolver Binding Context + +```typescript +// Wrong - 'this' won't work in arrow function resolvers with private data sources +const resolvers = { + Query: { + user: (_, { id }, context) => { + // ❌ 'this' is undefined in arrow functions + return this.userDataSource.getUser(id, context); + } + } +}; + +// Correct - use regular function for 'this' binding +const resolvers = { + Query: { + user(_, { id }, context) { + // ✅ 'this' correctly bound to component instance + return this.userDataSource.getUser(id, context); + } + } +}; +``` + +### ❌ Mixed Pattern Confusion + +```typescript +// Wrong - mixing patterns without understanding +class UserComponent extends GraphQLComponent { + private userDataSource: UserDataSource; + + constructor(options = {}) { + super({ + resolvers: { + Query: { + user(_, { id }, context) { + // ❌ Trying to use injected pattern with private data source + return context.dataSources.users.getUser(id); + + // ✅ Should be using private pattern + // return this.userDataSource.getUser(id, context); + } + } + }, + dataSources: [new UserDataSource()], // ❌ Redundant if using private pattern + ...options + }); + + this.userDataSource = new UserDataSource(); // ❌ Now have two instances + } +} +``` + +### ❌ Expecting dataSourceOverrides to Work with Private Data Sources + +```typescript +// Wrong - dataSourceOverrides only works with injected pattern +class UserComponent extends GraphQLComponent { + private userDataSource: UserDataSource; + + constructor(options = {}) { + super(options); + this.userDataSource = new UserDataSource(); + } +} + +// ❌ This will NOT override the private data source +const component = new UserComponent({ + dataSourceOverrides: [new MockUserDataSource()] // This only affects injected data sources +}); + +// ✅ Correct - design for dependency injection if you need overrides +class UserComponent extends GraphQLComponent { + private userDataSource: UserDataSource; + + constructor({ userDataSource = new UserDataSource(), ...options } = {}) { + super(options); + this.userDataSource = userDataSource; // Accept via constructor + } +} + +const component = new UserComponent({ + userDataSource: new MockUserDataSource() // Now this works +}); + +// Correct - choose one pattern consistently +class UserComponent extends GraphQLComponent { + private userDataSource: UserDataSource; + + constructor(options = {}) { + super({ + resolvers: { + Query: { + user(_, { id }, context) { + // ✅ Using private pattern consistently + return this.userDataSource.getUser(id, context); + } + } + }, + // ✅ No dataSources array when using private pattern + ...options + }); + + this.userDataSource = new UserDataSource(); + } +} +``` + +## Migration Guide + +### From v5.x to v6.x + +The data source system is largely unchanged between versions, but here are the key differences: + +#### Import Changes +```typescript +// v5.x +const GraphQLComponent = require('graphql-component'); + +// v6.x - TypeScript with proper imports +import GraphQLComponent, { + DataSourceDefinition, + ComponentContext, + IDataSource +} from 'graphql-component'; +``` + +#### Enhanced Type Safety +v6.x provides better TypeScript support: + +```typescript +// v6.x - More explicit typing +class UserDataSource implements DataSourceDefinition, IDataSource { + name = 'users'; + + async getUser(context: ComponentContext, id: string): Promise { + // Better type inference and checking + return null; + } +} +``` + +### Best Practices Summary + +#### General Principles +1. **Data sources don't call data sources**: Compose data in resolvers, not data sources +2. **Choose one pattern consistently**: Don't mix injected and private patterns in the same component +3. **Prefer regular methods**: Over arrow functions for better `this` binding +4. **Type your interfaces**: Define explicit interfaces for complex data sources +5. **Cache when appropriate**: Implement caching for expensive operations +6. **Handle errors gracefully**: Return null/undefined for missing data rather than throwing + +#### Injected Data Sources (Recommended) +7. **Always implement both interfaces**: `DataSourceDefinition` and `IDataSource` +8. **Context first**: Always make context the first parameter in data source methods +9. **Don't pass context manually**: Let the proxy handle context injection +10. **Use meaningful names**: The `name` property is used for identification +11. **Test with overrides**: Use `dataSourceOverrides` for testing + +#### Private Data Sources +12. **Use regular functions in resolvers**: Arrow functions break `this` binding +13. **Design for testability**: Accept data sources via constructor or create extension points +14. **Manual context passing**: Remember to pass context manually to private data source methods +15. **Use for delegation**: Ideal for accessing other component schemas + +#### When to Use Which Pattern + +**Use Injected Data Sources When:** +- ✅ Accessing external APIs or databases +- ✅ You need testing overrides (`dataSourceOverrides`) +- ✅ Different environments require different implementations +- ✅ Data sources might be shared across components +- ✅ You want runtime configuration flexibility +- ✅ Following dependency injection principles + +**Use Private Data Sources When:** +- ✅ Component delegation (accessing other component schemas) +- ✅ Component-specific internal logic that shouldn't be configurable +- ✅ You need direct control and accept the configuration limitations +- ✅ The data source is tightly coupled to the component implementation +- ❌ **Avoid if** you need testing overrides via configuration +- ❌ **Avoid if** you need environment-based data source swapping +- ❌ **Avoid if** you need runtime reconfiguration + +The data source system is one of the most powerful features of `graphql-component`, providing automatic context injection, type safety, and flexible testing capabilities. Understanding these patterns will help you build robust, maintainable GraphQL APIs. \ No newline at end of file diff --git a/README.md b/README.md index ad490f9..3d66a59 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Read more about the architecture principles in our [blog post](https://medium.co - 🔧 **Modular Schema Design**: Build schemas through composable components - 🔄 **Schema Stitching**: Merge multiple component schemas seamlessly - 🚀 **Apollo Federation Support**: Build federated subgraphs with component architecture -- 📦 **Data Source Management**: Simplified data source injection and overrides +- 📦 **Data Source Management**: Simplified data source injection and overrides ([guide](./DATASOURCES.md)) - 🛠️ **Flexible Configuration**: Extensive options for schema customization ## Installation @@ -27,12 +27,18 @@ npm install graphql-component ## Quick Start ```javascript +// CommonJS const GraphQLComponent = require('graphql-component'); -const { schema, context } = new GraphQLComponent({ +// ES Modules / TypeScript +import GraphQLComponent from 'graphql-component'; + +const component = new GraphQLComponent({ types, resolvers }); + +const { schema, context } = component; ``` ## Core Concepts @@ -56,7 +62,7 @@ const component = new GraphQLComponent({ }); ``` -This uses `@apollo/federation`'s `buildSubgraphSchema()` instead of `makeExecutableSchema()`. +This uses `@apollo/federation`'s `buildFederatedSchema()` instead of `makeExecutableSchema()`. ## API Reference @@ -78,7 +84,7 @@ new GraphQLComponent(options: IGraphQLComponentOptions) - `federation`: `boolean` - Enable Apollo Federation support (default: `false`) - `pruneSchema`: `boolean` - Enable schema pruning (default: `false`) - `pruneSchemaOptions`: `object` - Schema pruning options -- `transforms`: `Array` - Schema transformation functions +- `transforms`: `Array` - Schema transformation functions using `@graphql-tools/utils` ### Component Instance Properties @@ -96,6 +102,40 @@ interface IGraphQLComponent { } ``` +### Component Instance Methods + +#### dispose() + +Cleans up internal references and resources. Call this method when you're done with a component instance to help with garbage collection: + +```typescript +component.dispose(); +``` + +## Migration from v5.x to v6.x + +### delegateToComponent Removal + +In v6.0.0, `delegateToComponent` was removed. Use `@graphql-tools/delegate`'s `delegateToSchema` instead: + +```javascript +// Before (v5.x - removed) +// return delegateToComponent(targetComponent, { targetRootField: 'fieldName', args, context, info }); + +// After (v6.x+) +import { delegateToSchema } from '@graphql-tools/delegate'; + +return delegateToSchema({ + schema: targetComponent.schema, + fieldName: 'fieldName', + args, + context, + info +}); +``` + +For more complex delegation scenarios, refer to the [`@graphql-tools/delegate` documentation](https://the-guild.dev/graphql/tools/docs/schema-delegation). + ## Usage Examples ### Component Extension @@ -127,158 +167,103 @@ const server = new ApolloServer({ schema, context }); ### Data Sources -Data sources in `graphql-component` use a proxy-based approach for context injection. The library provides two key types to assist with correct implementation: +Data sources in `graphql-component` provide automatic context injection and type-safe data access. The library uses a proxy system to seamlessly inject context into your data source methods. ```typescript -// When implementing a data source: -class MyDataSource implements DataSourceDefinition { - name = 'MyDataSource'; - - // Context must be the first parameter when implementing - async getUserById(context: ComponentContext, id: string) { - // Use context for auth, config, etc. - return { id, name: 'User Name' }; - } -} - -// In resolvers, context is automatically injected: -const resolvers = { - Query: { - user(_, { id }, context) { - // Don't need to pass context - it's injected automatically - return context.dataSources.MyDataSource.getUserById(id); - } - } -} - -// Add to component: -new GraphQLComponent({ - types, - resolvers, - dataSources: [new MyDataSource()] -}); -``` - -#### Data Source Types - -- `DataSourceDefinition`: Interface for implementing data sources - methods must accept context as first parameter -- `DataSource`: Type representing data sources after proxy wrapping - context is automatically injected - -This type system ensures proper context handling while providing a clean API for resolver usage. - -#### TypeScript Example - -```typescript -import { - GraphQLComponent, +import GraphQLComponent, { DataSourceDefinition, - ComponentContext + ComponentContext, + IDataSource } from 'graphql-component'; -// Define your data source with proper types -class UsersDataSource implements DataSourceDefinition { +// Define your data source +class UsersDataSource implements DataSourceDefinition, IDataSource { name = 'users'; - // Static property - defaultRole = 'user'; - - // Context is required as first parameter when implementing - async getUserById(context: ComponentContext, id: string): Promise { - // Access context properties (auth, etc.) - const apiKey = context.config?.apiKey; - - // Implementation details... - return { id, name: 'User Name', role: this.defaultRole }; - } - - async getUsersByRole(context: ComponentContext, role: string): Promise { - // Implementation details... - return [ - { id: '1', name: 'User 1', role }, - { id: '2', name: 'User 2', role } - ]; + // Context is automatically injected as first parameter + async getUserById(context: ComponentContext, id: string) { + // Access context for auth, config, etc. + const token = context.auth?.token; + return fetchUser(id, token); } } -// In resolvers, the context is automatically injected +// Use in resolvers - context injection is automatic const resolvers = { Query: { - user: (_, { id }, context) => { - // No need to pass context - it's injected by the proxy + user(_, { id }, context) { + // No need to pass context manually - it's injected automatically return context.dataSources.users.getUserById(id); - }, - usersByRole: (_, { role }, context) => { - // No need to pass context - it's injected by the proxy - return context.dataSources.users.getUsersByRole(role); } } }; -// Component configuration -const usersComponent = new GraphQLComponent({ - types: ` - type User { - id: ID! - name: String! - role: String! - } - - type Query { - user(id: ID!): User - usersByRole(role: String!): [User] - } - `, +// Add to component +const component = new GraphQLComponent({ + types, resolvers, dataSources: [new UsersDataSource()] }); ``` -#### Data Source Overrides +**Key Concepts:** +- **Two Patterns**: Injected data sources (via context) or private data sources (via `this`) +- **Implementation**: Context must be the first parameter in injected data source methods +- **Usage**: Context is automatically injected for injected data sources +- **Resolver Binding**: Resolvers are bound to component instances, enabling `this` access +- **Testing**: Use `dataSourceOverrides` for injected sources, class extension for private sources +- **Type Safety**: TypeScript interfaces ensure correct implementation + +For comprehensive documentation including both patterns, advanced usage, testing strategies, and common gotchas, see the **[Data Sources Guide](./DATASOURCES.md)**. -You can override data sources when needed (for testing or extending functionality). The override must follow the same interface: +### Context Middleware + +Components support context middleware that runs before the component's context is built. This is useful for authentication, logging, or transforming context: ```typescript -// For testing - create a mock data source -class MockUsersDataSource implements DataSourceDefinition { - name = 'users'; - defaultRole = 'admin'; - - async getUserById(context: ComponentContext, id: string) { - return { id, name: 'Mock User', role: this.defaultRole }; - } - - async getUsersByRole(context: ComponentContext, role: string) { - return [{ id: 'mock', name: 'Mock User', role }]; - } -} +const component = new GraphQLComponent({ + types, + resolvers +}); -// Use the component with overrides -const testComponent = new GraphQLComponent({ - imports: [usersComponent], - dataSourceOverrides: [new MockUsersDataSource()] +// Add authentication middleware +component.context.use('auth', async (context) => { + const user = await authenticate(context.req?.headers?.authorization); + return { ...context, user }; }); -// In tests -const context = await testComponent.context({}); -const mockUser = await context.dataSources.users.getUserById('any-id'); -// mockUser will be { id: 'any-id', name: 'Mock User', role: 'admin' } +// Add logging middleware +component.context.use('logging', async (context) => { + console.log('Building context for request', context.requestId); + return context; +}); + +// Use the context (middleware runs automatically) +const context = await component.context({ req, requestId: '123' }); +// Context now includes user and logs the request ``` +Middleware runs in the order it's added and each middleware receives the transformed context from the previous middleware. + ## Examples -The repository includes example implementations: +The repository includes working example implementations demonstrating different use cases: ### Local Schema Composition ```bash npm run start-composition ``` +This example shows how to compose multiple GraphQL components into a single schema using schema stitching. ### Federation Example ```bash npm run start-federation ``` +This example demonstrates building Apollo Federation subgraphs using GraphQL components. + +Both examples are accessible at `http://localhost:4000/graphql` when running. -Both examples are accessible at `http://localhost:4000/graphql` +You can find the complete example code in the [`examples/`](./examples/) directory. ## Debugging diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index b681b94..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { GraphQLResolveInfo, GraphQLSchema } from 'graphql'; -import { IResolvers, PruneSchemaOptions, TypeSource, SchemaMapper } from '@graphql-tools/utils'; -import { IMocks } from '@graphql-tools/mock'; -import { SubschemaConfig } from '@graphql-tools/delegate'; -export type ResolverFunction = (_: any, args: any, ctx: any, info: GraphQLResolveInfo) => any; -export interface IGraphQLComponentConfigObject { - component: IGraphQLComponent; - configuration?: SubschemaConfig; -} -export interface ComponentContext extends Record { - dataSources: DataSourceMap; -} -export type ContextFunction = ((context: Record) => any); -export interface IDataSource { - name?: string; - [key: string | symbol]: any; -} -/** - * Type for implementing data sources - * When defining a data source class, methods should accept context as their first parameter - * @example - * class MyDataSource { - * name = 'MyDataSource'; - * - * // Context is required as first parameter when implementing - * getData(context: ComponentContext, id: string) { - * return { id }; - * } - * } - */ -export type DataSourceDefinition = { - [P in keyof T]: T[P] extends Function ? (context: ComponentContext, ...args: any[]) => any : T[P]; -}; -/** - * Type for consuming data sources in resolvers - * When using a data source method, the context is automatically injected - * @example - * // In a resolver: - * Query: { - * getData(_, { id }, context) { - * // Context is automatically injected, so you don't pass it - * return context.dataSources.MyDataSource.getData(id); - * } - * } - */ -export type DataSource = { - [P in keyof T]: T[P] extends (context: ComponentContext, ...p: infer P) => infer R ? (...p: P) => R : T[P]; -}; -export type DataSourceMap = { - [key: string]: IDataSource; -}; -export type DataSourceInjectionFunction = ((context: Record) => DataSourceMap); -export interface IContextConfig { - namespace: string; - factory: ContextFunction; -} -export interface IContextWrapper extends ContextFunction { - use: (name: string | ContextFunction | null, fn?: ContextFunction | string) => void; -} -export interface IGraphQLComponentOptions { - types?: TypeSource; - resolvers?: IResolvers; - mocks?: boolean | IMocks; - imports?: (IGraphQLComponent | IGraphQLComponentConfigObject)[]; - context?: IContextConfig; - dataSources?: IDataSource[]; - dataSourceOverrides?: IDataSource[]; - pruneSchema?: boolean; - pruneSchemaOptions?: PruneSchemaOptions; - federation?: boolean; - transforms?: SchemaMapper[]; -} -export interface IGraphQLComponent { - readonly name: string; - readonly schema: GraphQLSchema; - readonly context: IContextWrapper; - readonly types: TypeSource; - readonly resolvers: IResolvers; - readonly imports?: (IGraphQLComponent | IGraphQLComponentConfigObject)[]; - readonly dataSources?: IDataSource[]; - readonly dataSourceOverrides?: IDataSource[]; - federation?: boolean; -} -/** - * GraphQLComponent class for building modular GraphQL schemas - * @template TContextType - The type of the context object - * @implements {IGraphQLComponent} - */ -export default class GraphQLComponent implements IGraphQLComponent { - _schema: GraphQLSchema; - _types: TypeSource; - _resolvers: IResolvers; - _mocks: boolean | IMocks; - _imports: IGraphQLComponentConfigObject[]; - _context: ContextFunction; - _dataSources: IDataSource[]; - _dataSourceOverrides: IDataSource[]; - _pruneSchema: boolean; - _pruneSchemaOptions: PruneSchemaOptions; - _federation: boolean; - _dataSourceContextInject: DataSourceInjectionFunction; - _transforms: SchemaMapper[]; - private _transformedSchema; - constructor({ types, resolvers, mocks, imports, context, dataSources, dataSourceOverrides, pruneSchema, pruneSchemaOptions, federation, transforms }: IGraphQLComponentOptions); - get context(): IContextWrapper; - get name(): string; - get schema(): GraphQLSchema; - get types(): TypeSource; - get resolvers(): IResolvers; - get imports(): IGraphQLComponentConfigObject[]; - get dataSources(): IDataSource[]; - get dataSourceOverrides(): IDataSource[]; - set federation(flag: boolean); - get federation(): boolean; - dispose(): void; - private transformSchema; - private validateConfig; -} diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 5062b24..0000000 --- a/dist/index.js +++ /dev/null @@ -1,351 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const debug_1 = __importDefault(require("debug")); -const federation_1 = require("@apollo/federation"); -const graphql_1 = require("graphql"); -const merge_1 = require("@graphql-tools/merge"); -const utils_1 = require("@graphql-tools/utils"); -const schema_1 = require("@graphql-tools/schema"); -const stitch_1 = require("@graphql-tools/stitch"); -const mock_1 = require("@graphql-tools/mock"); -const debug = (0, debug_1.default)('graphql-component'); -/** - * GraphQLComponent class for building modular GraphQL schemas - * @template TContextType - The type of the context object - * @implements {IGraphQLComponent} - */ -class GraphQLComponent { - _schema; - _types; - _resolvers; - _mocks; - _imports; - _context; - _dataSources; - _dataSourceOverrides; - _pruneSchema; - _pruneSchemaOptions; - _federation; - _dataSourceContextInject; - _transforms; - _transformedSchema; - constructor({ types, resolvers, mocks, imports, context, dataSources, dataSourceOverrides, pruneSchema, pruneSchemaOptions, federation, transforms }) { - this._types = Array.isArray(types) ? types : [types]; - this._resolvers = bindResolvers(this, resolvers); - this._mocks = mocks; - this._federation = federation; - this._transforms = transforms; - this._dataSources = dataSources || []; - this._dataSourceOverrides = dataSourceOverrides || []; - this._dataSourceContextInject = createDataSourceContextInjector(this._dataSources, this._dataSourceOverrides); - this._pruneSchema = pruneSchema; - this._pruneSchemaOptions = pruneSchemaOptions; - this._imports = imports && imports.length > 0 ? imports.map((i) => { - if (i instanceof GraphQLComponent) { - if (this._federation === true) { - i.federation = true; - } - return { component: i }; - } - else { - const importConfiguration = i; - if (this._federation === true) { - importConfiguration.component.federation = true; - } - return importConfiguration; - } - }) : []; - this._context = async (globalContext) => { - //TODO: currently the context injected into data sources won't have data sources on it - const ctx = { - dataSources: this._dataSourceContextInject(globalContext) - }; - for (const { component } of this.imports) { - const { dataSources, ...importedContext } = await component.context(globalContext); - Object.assign(ctx.dataSources, dataSources); - Object.assign(ctx, importedContext); - } - if (context) { - debug(`building ${context.namespace} context`); - if (!ctx[context.namespace]) { - ctx[context.namespace] = {}; - } - Object.assign(ctx[context.namespace], await context.factory.call(this, globalContext)); - } - return ctx; - }; - this.validateConfig({ types, imports, mocks, federation }); - } - get context() { - const contextFn = async (context) => { - debug(`building root context`); - const middleware = contextFn._middleware || []; - for (const { name, fn } of middleware) { - debug(`applying ${name} middleware`); - context = await fn(context); - } - const componentContext = await this._context(context); - const globalContext = { - ...context, - ...componentContext - }; - return globalContext; - }; - contextFn._middleware = []; - contextFn.use = function (name, fn) { - if (typeof name === 'function') { - fn = name; - name = 'unknown'; - } - debug(`adding ${name} middleware`); - contextFn._middleware.push({ name, fn }); - return contextFn; - }; - return contextFn; - } - get name() { - return this.constructor.name; - } - get schema() { - try { - if (this._schema) { - return this._schema; - } - let makeSchema; - if (this._federation) { - makeSchema = federation_1.buildFederatedSchema; - } - else { - makeSchema = schema_1.makeExecutableSchema; - } - if (this._imports.length > 0) { - // iterate through the imports and construct subschema configuration objects - const subschemas = this._imports.map((imp) => { - const { component, configuration = {} } = imp; - return { - schema: component.schema, - ...configuration - }; - }); - // construct an aggregate schema from the schemas of imported - // components and this component's types/resolvers (if present) - this._schema = (0, stitch_1.stitchSchemas)({ - subschemas, - typeDefs: this._types, - resolvers: this._resolvers, - mergeDirectives: true - }); - } - else { - const schemaConfig = { - typeDefs: (0, merge_1.mergeTypeDefs)(this._types), - resolvers: this._resolvers - }; - this._schema = makeSchema(schemaConfig); - } - if (this._transforms) { - this._schema = this.transformSchema(this._schema, this._transforms); - } - if (this._mocks !== undefined && typeof this._mocks === 'boolean' && this._mocks === true) { - debug(`adding default mocks to the schema for ${this.name}`); - // if mocks are a boolean support simply applying default mocks - this._schema = (0, mock_1.addMocksToSchema)({ schema: this._schema, preserveResolvers: true }); - } - else if (this._mocks !== undefined && typeof this._mocks === 'object') { - debug(`adding custom mocks to the schema for ${this.name}`); - // else if mocks is an object, that means the user provided - // custom mocks, with which we pass them to addMocksToSchema so they are applied - this._schema = (0, mock_1.addMocksToSchema)({ schema: this._schema, mocks: this._mocks, preserveResolvers: true }); - } - if (this._pruneSchema) { - debug(`pruning the schema for ${this.name}`); - this._schema = (0, utils_1.pruneSchema)(this._schema, this._pruneSchemaOptions); - } - debug(`created schema for ${this.name}`); - return this._schema; - } - catch (error) { - debug(`Error creating schema for ${this.name}: ${error}`); - throw new Error(`Failed to create schema for component ${this.name}: ${error.message}`); - } - } - get types() { - return this._types; - } - get resolvers() { - return this._resolvers; - } - get imports() { - return this._imports; - } - get dataSources() { - return this._dataSources; - } - get dataSourceOverrides() { - return this._dataSourceOverrides; - } - set federation(flag) { - this._federation = flag; - } - get federation() { - return this._federation; - } - dispose() { - this._schema = null; - this._types = null; - this._resolvers = null; - this._imports = null; - this._dataSources = null; - this._dataSourceOverrides = null; - } - transformSchema(schema, transforms) { - if (this._transformedSchema) { - return this._transformedSchema; - } - const functions = {}; - const mapping = {}; - for (const transform of transforms) { - for (const [key, fn] of Object.entries(transform)) { - if (!mapping[key]) { - functions[key] = []; - let result = undefined; - mapping[key] = function (...args) { - while (functions[key].length) { - const mapper = functions[key].shift(); - result = mapper(...args); - if (!result) { - break; - } - } - return result; - }; - } - functions[key].push(fn); - } - } - this._transformedSchema = (0, utils_1.mapSchema)(schema, mapping); - return this._transformedSchema; - } - validateConfig(options) { - if (options.federation && !options.types) { - throw new Error('Federation requires type definitions'); - } - if (options.mocks && typeof options.mocks !== 'boolean' && typeof options.mocks !== 'object') { - throw new Error('mocks must be either boolean or object'); - } - } -} -exports.default = GraphQLComponent; -// For backward compatibility -module.exports = GraphQLComponent; -/** - * Wraps data sources with a proxy that intercepts calls to data source methods and injects the current context - * @param {IDataSource[]} dataSources - * @param {IDataSource[]} dataSourceOverrides - * @returns {DataSourceInjectionFunction} a function that returns a map of data sources with methods that have been intercepted - */ -const createDataSourceContextInjector = (dataSources, dataSourceOverrides) => { - const intercept = (instance, context) => { - debug(`intercepting ${instance.constructor.name}`); - return new Proxy(instance, { - get(target, key) { - if (typeof target[key] !== 'function' || key === instance.constructor.name) { - return target[key]; - } - const original = target[key]; - return function (...args) { - return original.call(instance, context, ...args); - }; - } - }); - }; - return (context = {}) => { - const proxiedDataSources = {}; - // Inject data sources - for (const dataSource of dataSources) { - proxiedDataSources[dataSource.name || dataSource.constructor.name] = intercept(dataSource, context); - } - // Override data sources - for (const dataSourceOverride of dataSourceOverrides) { - proxiedDataSources[dataSourceOverride.name || dataSourceOverride.constructor.name] = intercept(dataSourceOverride, context); - } - return proxiedDataSources; - }; -}; -/** - * memoizes resolver functions such that calls of an identical resolver (args/context/path) within the same request context are avoided - * @param {string} parentType - the type whose field resolver is being - * wrapped/memoized - * @param {string} fieldName - the field on the parentType whose resolver - * function is being wrapped/memoized - * @param {function} resolve - the resolver function that parentType. - * fieldName is mapped to - * @returns {function} a function that wraps the input resolver function and - * whose closure scope contains a WeakMap to achieve memoization of the wrapped - * input resolver function - */ -const memoize = function (parentType, fieldName, resolve) { - const _cache = new WeakMap(); - return function _memoizedResolver(_, args, context, info) { - const path = info && info.path && info.path.key; - const key = `${path}_${JSON.stringify(args)}`; - debug(`executing ${parentType}.${fieldName}`); - let cached = _cache.get(context); - if (cached && cached[key]) { - debug(`return cached result of memoized ${parentType}.${fieldName}`); - return cached[key]; - } - if (!cached) { - cached = {}; - } - const result = resolve(_, args, context, info); - cached[key] = result; - _cache.set(context, cached); - debug(`cached ${parentType}.${fieldName}`); - return result; - }; -}; -/** - * make 'this' in resolver functions equal to the input bindContext - * @param {Object} bind - the object context to bind to resolver functions - * @param {Object} resolvers - the resolver map containing the resolver - * functions to bind - * @returns {Object} - an object identical in structure to the input resolver - * map, except with resolver function bound to the input argument bind - */ -const bindResolvers = function (bindContext, resolvers = {}) { - const boundResolvers = {}; - for (const [type, fields] of Object.entries(resolvers)) { - // dont bind an object that is an instance of a graphql scalar - if (fields instanceof graphql_1.GraphQLScalarType) { - debug(`not binding ${type}'s fields since ${type}'s fields are an instance of GraphQLScalarType`); - boundResolvers[type] = fields; - continue; - } - if (!boundResolvers[type]) { - boundResolvers[type] = {}; - } - for (const [field, resolver] of Object.entries(fields)) { - if (['Query', 'Mutation'].indexOf(type) > -1) { - debug(`memoized ${type}.${field}`); - boundResolvers[type][field] = memoize(type, field, resolver.bind(bindContext)); - } - else { - // only bind resolvers that are functions - if (typeof resolver === 'function') { - debug(`binding ${type}.${field}`); - boundResolvers[type][field] = resolver.bind(bindContext); - } - else { - debug(`not binding ${type}.${field} since ${field} is not mapped to a function`); - boundResolvers[type][field] = resolver; - } - } - } - } - return boundResolvers; -}; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxrREFBZ0M7QUFDaEMsbURBQTBEO0FBQzFELHFDQUErRTtBQUUvRSxnREFBcUQ7QUFDckQsZ0RBTzhCO0FBQzlCLGtEQUE2RDtBQUM3RCxrREFBc0Q7QUFDdEQsOENBQStEO0FBRy9ELE1BQU0sS0FBSyxHQUFHLElBQUEsZUFBVyxFQUFDLG1CQUFtQixDQUFDLENBQUM7QUE2Ri9DOzs7O0dBSUc7QUFDSCxNQUFxQixnQkFBZ0I7SUFDbkMsT0FBTyxDQUFnQjtJQUN2QixNQUFNLENBQWE7SUFDbkIsVUFBVSxDQUFnQztJQUMxQyxNQUFNLENBQW1CO0lBQ3pCLFFBQVEsQ0FBa0M7SUFDMUMsUUFBUSxDQUFrQjtJQUMxQixZQUFZLENBQWdCO0lBQzVCLG9CQUFvQixDQUFnQjtJQUNwQyxZQUFZLENBQVU7SUFDdEIsbUJBQW1CLENBQW9CO0lBQ3ZDLFdBQVcsQ0FBVTtJQUNyQix3QkFBd0IsQ0FBOEI7SUFDdEQsV0FBVyxDQUFnQjtJQUNuQixrQkFBa0IsQ0FBZ0I7SUFFMUMsWUFBWSxFQUNWLEtBQUssRUFDTCxTQUFTLEVBQ1QsS0FBSyxFQUNMLE9BQU8sRUFDUCxPQUFPLEVBQ1AsV0FBVyxFQUNYLG1CQUFtQixFQUNuQixXQUFXLEVBQ1gsa0JBQWtCLEVBQ2xCLFVBQVUsRUFDVixVQUFVLEVBQ2U7UUFFekIsSUFBSSxDQUFDLE1BQU0sR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFckQsSUFBSSxDQUFDLFVBQVUsR0FBRyxhQUFhLENBQUMsSUFBSSxFQUFFLFNBQVMsQ0FBQyxDQUFDO1FBRWpELElBQUksQ0FBQyxNQUFNLEdBQUcsS0FBSyxDQUFDO1FBRXBCLElBQUksQ0FBQyxXQUFXLEdB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 \ No newline at end of file diff --git a/package.json b/package.json index e116915..a98102b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-component", - "version": "6.0.1", + "version": "6.0.2", "description": "Build, customize and compose GraphQL schemas in a componentized fashion", "keywords": [ "graphql", diff --git a/src/index.ts b/src/index.ts index f0f9727..90c9904 100644 --- a/src/index.ts +++ b/src/index.ts @@ -129,6 +129,7 @@ export default class GraphQLComponent): Promise => { - //TODO: currently the context injected into data sources won't have data sources on it + //BREAKING: The context injected into data sources won't have data sources on it const ctx = { dataSources: this._dataSourceContextInject(globalContext) }; - for (const { component } of this.imports) { - const { dataSources, ...importedContext } = await component.context(globalContext); - Object.assign(ctx.dataSources, dataSources); - Object.assign(ctx, importedContext); + // Only process imports if they exist + if (this._imports.length > 0) { + // Process imports in parallel if they're independent + const importPromises = this._imports.map(async ({ component }) => { + const importContext = await component.context(globalContext); + return importContext; + }); + + const importResults = await Promise.all(importPromises); + + // Merge results efficiently + for (const { dataSources, ...importedContext } of importResults) { + Object.assign(ctx.dataSources, dataSources); + Object.assign(ctx, importedContext); + } } + // Handle namespace context if present if (context) { debug(`building ${context.namespace} context`); @@ -200,7 +212,8 @@ export default class GraphQLComponent): Promise => { debug(`building root context`); - const middleware: MiddlewareEntry[] = (contextFn as any)._middleware || []; + let processedContext = context; - for (const { name, fn } of middleware) { - debug(`applying ${name} middleware`); - context = await fn(context); + // Apply middleware more efficiently + if (this._middleware.length > 0) { + for (const { name, fn } of this._middleware) { + debug(`applying ${name} middleware`); + processedContext = await fn(processedContext); + } } - const componentContext = await this._context(context); - - const globalContext = { - ...context, - ...componentContext - }; + const componentContext = await this._context(processedContext); - return globalContext; + // More efficient object composition + return Object.assign({}, processedContext, componentContext); }; - contextFn._middleware = []; - - contextFn.use = function (name: string, fn: ContextFunction): IContextWrapper { + contextFn.use = (name: string | ContextFunction, fn?: ContextFunction): IContextWrapper => { if (typeof name === 'function') { fn = name; name = 'unknown'; } debug(`adding ${name} middleware`); - contextFn._middleware.push({ name, fn }); + this._middleware.push({ name: name as string, fn: fn! }); return contextFn; }; @@ -248,6 +258,8 @@ export default class GraphQLComponent { + + t.test('should merge data sources from imported components', async (assert) => { + class ParentDataSource { + name = 'parent'; + getParentData(context: any, id: string) { + return `parent-${id}`; + } + } + + class ChildDataSource { + name = 'child'; + getChildData(context: any, id: string) { + return `child-${id}`; + } + } + + const childComponent = new GraphQLComponent({ + types: `type Query { childQuery: String }`, + dataSources: [new ChildDataSource()] + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parentQuery: String }`, + dataSources: [new ParentDataSource()], + imports: [childComponent] + }); + + const context = await parentComponent.context({ requestId: 'test' }); + + // Both data sources should be available + assert.ok(context.dataSources.parent, 'parent data source available'); + assert.ok(context.dataSources.child, 'child data source available'); + + // Both should work with context injection + const parentResult = context.dataSources.parent.getParentData('123'); + const childResult = context.dataSources.child.getChildData('456'); + + assert.equal(parentResult, 'parent-123', 'parent data source works'); + assert.equal(childResult, 'child-456', 'child data source works'); + assert.end(); + }); + + t.test('should handle async context factories in imports', async (assert) => { + const childComponent = new GraphQLComponent({ + types: `type Query { child: String }`, + context: { + namespace: 'child', + factory: async (globalContext) => { + // Simulate async work + await new Promise(resolve => setTimeout(resolve, 10)); + return { + asyncData: 'child-async', + globalValue: globalContext.test + }; + } + } + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + context: { + namespace: 'parent', + factory: async (globalContext) => { + await new Promise(resolve => setTimeout(resolve, 5)); + return { + asyncData: 'parent-async', + globalValue: globalContext.test + }; + } + }, + imports: [childComponent] + }); + + const context = await parentComponent.context({ test: 'global-value' }); + + assert.equal(context.child.asyncData, 'child-async', 'child async context built'); + assert.equal(context.parent.asyncData, 'parent-async', 'parent async context built'); + assert.equal(context.child.globalValue, 'global-value', 'child received global context'); + assert.equal(context.parent.globalValue, 'global-value', 'parent received global context'); + assert.end(); + }); + + t.test('should handle deep import chains', async (assert) => { + // Create a chain: grandparent -> parent -> child + const childComponent = new GraphQLComponent({ + types: `type Query { child: String }`, + context: { + namespace: 'child', + factory: async () => ({ level: 'child' }) + } + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + context: { + namespace: 'parent', + factory: async () => ({ level: 'parent' }) + }, + imports: [childComponent] + }); + + const grandparentComponent = new GraphQLComponent({ + types: `type Query { grandparent: String }`, + context: { + namespace: 'grandparent', + factory: async () => ({ level: 'grandparent' }) + }, + imports: [parentComponent] + }); + + const context = await grandparentComponent.context({}); + + // All levels should be present + assert.equal(context.child.level, 'child', 'child context built'); + assert.equal(context.parent.level, 'parent', 'parent context built'); + assert.equal(context.grandparent.level, 'grandparent', 'grandparent context built'); + assert.end(); + }); + + t.test('should handle context factory errors gracefully', async (assert) => { + const failingComponent = new GraphQLComponent({ + types: `type Query { failing: String }`, + context: { + namespace: 'failing', + factory: async () => { + throw new Error('Context factory failed'); + } + } + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + imports: [failingComponent] + }); + + try { + await parentComponent.context({}); + assert.fail('Expected error to be thrown'); + } catch (error) { + assert.ok(error.message.includes('Context factory failed'), 'error propagated correctly'); + } + assert.end(); + }); + + t.test('should handle multiple imports with same data source names', async (assert) => { + class TestDataSource { + constructor(public suffix: string) {} + name = 'test'; + getData(context: any, id: string) { + return `${this.suffix}-${id}`; + } + } + + const component1 = new GraphQLComponent({ + types: `type Query { comp1: String }`, + dataSources: [new TestDataSource('comp1')] + }); + + const component2 = new GraphQLComponent({ + types: `type Query { comp2: String }`, + dataSources: [new TestDataSource('comp2')] + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + imports: [component1, component2] // Both have 'test' data source + }); + + const context = await parentComponent.context({}); + + // Last one should win (component2) + const result = context.dataSources.test.getData('123'); + assert.equal(result, 'comp2-123', 'last imported data source wins in case of name collision'); + assert.end(); + }); + + t.test('should process imports in parallel and maintain correct timing', async (assert) => { + const timings: { component: string; start: number; end: number }[] = []; + + const createDelayedComponent = (name: string, delay: number) => { + return new GraphQLComponent({ + types: `type Query { ${name}: String }`, + context: { + namespace: name, + factory: async () => { + const start = Date.now(); + timings.push({ component: name, start, end: 0 }); + await new Promise(resolve => setTimeout(resolve, delay)); + const end = Date.now(); + timings[timings.length - 1].end = end; + return { processed: true }; + } + } + }); + }; + + const comp1 = createDelayedComponent('comp1', 30); + const comp2 = createDelayedComponent('comp2', 20); + const comp3 = createDelayedComponent('comp3', 10); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + imports: [comp1, comp2, comp3] + }); + + const overallStart = Date.now(); + const context = await parentComponent.context({}); + const overallEnd = Date.now(); + + // All contexts should be built + assert.ok(context.comp1.processed, 'comp1 processed'); + assert.ok(context.comp2.processed, 'comp2 processed'); + assert.ok(context.comp3.processed, 'comp3 processed'); + + // Should complete faster than sequential (60ms total) due to parallelization + const totalTime = overallEnd - overallStart; + assert.ok(totalTime < 50, `parallel processing completed in ${totalTime}ms (expected < 50ms)`); + + // All imports should start around the same time (within 5ms) + const startTimes = timings.map(t => t.start); + const maxStartDiff = Math.max(...startTimes) - Math.min(...startTimes); + assert.ok(maxStartDiff < 5, `imports started concurrently (max diff: ${maxStartDiff}ms)`); + + assert.end(); + }); + + t.test('should handle imports with both data sources and context namespaces', async (assert) => { + class SharedDataSource { + name = 'shared'; + getData(context: any, id: string) { + return `shared-${context.namespace || 'unknown'}-${id}`; + } + } + + const component1 = new GraphQLComponent({ + types: `type Query { comp1: String }`, + context: { + namespace: 'comp1', + factory: async () => ({ namespace: 'comp1' }) + }, + dataSources: [new SharedDataSource()] + }); + + const component2 = new GraphQLComponent({ + types: `type Query { comp2: String }`, + context: { + namespace: 'comp2', + factory: async () => ({ namespace: 'comp2' }) + } + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + imports: [component1, component2] + }); + + const context = await parentComponent.context({}); + + // Both namespaces should be present + assert.equal(context.comp1.namespace, 'comp1', 'comp1 namespace context'); + assert.equal(context.comp2.namespace, 'comp2', 'comp2 namespace context'); + + // Data source should be available + assert.ok(context.dataSources.shared, 'shared data source available'); + + assert.end(); + }); + + t.test('should preserve data source method binding across async operations', async (assert) => { + class InstanceDataSource { + name = 'instance'; + private instanceData = 'bound-correctly'; + + async getData(context: any, id: string) { + // Simulate async work to test binding preservation + await new Promise(resolve => setTimeout(resolve, 1)); + return `${this.instanceData}-${id}`; + } + } + + const childComponent = new GraphQLComponent({ + types: `type Query { child: String }`, + dataSources: [new InstanceDataSource()] + }); + + const parentComponent = new GraphQLComponent({ + types: `type Query { parent: String }`, + imports: [childComponent] + }); + + const context = await parentComponent.context({}); + + // Method binding should be preserved even after async import processing + const result = await context.dataSources.instance.getData('test'); + assert.equal(result, 'bound-correctly-test', 'method binding preserved across async operations'); + assert.end(); + }); + + t.end(); +}); \ No newline at end of file diff --git a/test/performance-regression.ts b/test/performance-regression.ts new file mode 100644 index 0000000..c90ed08 --- /dev/null +++ b/test/performance-regression.ts @@ -0,0 +1,305 @@ +import test from 'tape'; +import GraphQLComponent from '../src/index'; +import { graphql } from 'graphql'; + +test('Performance Optimization Regression Tests', (t) => { + + t.test('Data Source Proxy: Context Updates Correctly', async (assert) => { + class TestDataSource { + name = 'test'; + getData(context: any, id: string) { + return `${context.requestId}-${id}`; + } + } + + const component = new GraphQLComponent({ + types: `type Query { test(id: String!): String }`, + resolvers: { + Query: { + test(_, { id }, context) { + return context.dataSources.test.getData(id); + } + } + }, + dataSources: [new TestDataSource()] + }); + + // Multiple requests with different contexts should work correctly + const context1 = await component.context({ requestId: 'req1' }); + const context2 = await component.context({ requestId: 'req2' }); + + const result1 = context1.dataSources.test.getData('test1'); + const result2 = context2.dataSources.test.getData('test2'); + + assert.equal(result1, 'req1-test1', 'first context works'); + assert.equal(result2, 'req2-test2', 'second context works'); + assert.notEqual(result1, result2, 'contexts are properly isolated'); + assert.end(); + }); + + t.test('Data Source Proxy: Method Binding Preserved', async (assert) => { + class TestDataSource { + name = 'test'; + instanceValue = 'instance-data'; + + getData(context: any) { + return this.instanceValue; // 'this' should be bound correctly + } + } + + const component = new GraphQLComponent({ + types: `type Query { test: String }`, + dataSources: [new TestDataSource()] + }); + + const context = await component.context({}); + const result = context.dataSources.test.getData(); + + assert.equal(result, 'instance-data', 'method binding preserved'); + assert.end(); + }); + + t.test('Memoization: Complex Object Arguments', async (assert) => { + let callCount = 0; + + const component = new GraphQLComponent({ + types: ` + type Query { + search(filter: SearchFilter): String + } + input SearchFilter { + name: String + tags: [String] + range: IntRange + } + input IntRange { + min: Int + max: Int + } + `, + resolvers: { + Query: { + search(_, { filter }) { + callCount++; + return `Found: ${filter.name}`; + } + } + } + }); + + const schema = component.schema; + const query = ` + query($filter: SearchFilter) { + search(filter: $filter) + } + `; + + const complexFilter = { + name: "test", + tags: ["tag1", "tag2"], + range: { min: 1, max: 100 } + }; + + const context = { requestId: 'test' }; + + // Same complex arguments should be memoized + await graphql({ schema, source: query, variableValues: { filter: complexFilter }, contextValue: context }); + await graphql({ schema, source: query, variableValues: { filter: complexFilter }, contextValue: context }); + + assert.equal(callCount, 1, 'complex object arguments are properly memoized'); + assert.end(); + }); + + t.test('Memoization: Simple Argument Handling', async (assert) => { + let calls: string[] = []; + + const component = new GraphQLComponent({ + types: `type Query { test(data: String): String }`, + resolvers: { + Query: { + test(_, { data }) { + calls.push(data); + return `result-${data}`; + } + } + } + }); + + const schema = component.schema; + const context = { requestId: 'test' }; + + // Test with simple distinct arguments + const testCases = ['data1', 'data2', 'data3']; + + for (const testData of testCases) { + await graphql({ + schema, + source: `{ test(data: "${testData}") }`, + contextValue: context + }); + } + + assert.equal(calls.length, testCases.length, 'each unique argument resulted in resolver call'); + assert.deepEqual(calls, testCases, 'all unique calls were made'); + assert.end(); + }); + + t.test('Context Building: Parallel Import Processing', async (assert) => { + let initOrder: string[] = []; + + class DelayedDataSource { + constructor(public name: string, public delay: number) {} + + async init(context: any) { + await new Promise(resolve => setTimeout(resolve, this.delay)); + initOrder.push(this.name); + return { initialized: true }; + } + } + + const component1 = new GraphQLComponent({ + types: `type Query { test1: String }`, + context: { + namespace: 'comp1', + factory: async () => { + const ds = new DelayedDataSource('comp1', 50); + await ds.init({}); + return { comp1Data: true }; + } + } + }); + + const component2 = new GraphQLComponent({ + types: `type Query { test2: String }`, + context: { + namespace: 'comp2', + factory: async () => { + const ds = new DelayedDataSource('comp2', 30); + await ds.init({}); + return { comp2Data: true }; + } + } + }); + + const component3 = new GraphQLComponent({ + types: `type Query { test3: String }`, + context: { + namespace: 'comp3', + factory: async () => { + const ds = new DelayedDataSource('comp3', 10); + await ds.init({}); + return { comp3Data: true }; + } + } + }); + + const mainComponent = new GraphQLComponent({ + types: `type Query { main: String }`, + imports: [component1, component2, component3] + }); + + const startTime = Date.now(); + const context = await mainComponent.context({}); + const endTime = Date.now(); + + // Should complete faster than sequential processing (90ms) due to parallelization + const totalTime = endTime - startTime; + assert.ok(totalTime < 80, `parallel processing faster than sequential: ${totalTime}ms < 80ms`); + + // All components should be initialized + assert.ok(context.comp1?.comp1Data, 'component 1 context built'); + assert.ok(context.comp2?.comp2Data, 'component 2 context built'); + assert.ok(context.comp3?.comp3Data, 'component 3 context built'); + + // Faster components should finish first (parallel execution) + assert.equal(initOrder[0], 'comp3', 'fastest component finished first'); + assert.equal(initOrder[2], 'comp1', 'slowest component finished last'); + + assert.end(); + }); + + t.test('Context Building: Middleware Order Preserved', async (assert) => { + const executionOrder: string[] = []; + + const component = new GraphQLComponent({ + types: `type Query { test: String }` + }); + + const contextFn = component.context; + + // Add middleware in specific order + contextFn.use('first', async (ctx) => { + executionOrder.push('first'); + return { ...ctx, value: 1 }; + }); + + contextFn.use('second', async (ctx) => { + executionOrder.push('second'); + return { ...ctx, value: (ctx.value as number) + 1 }; + }); + + contextFn.use('third', async (ctx) => { + executionOrder.push('third'); + return { ...ctx, value: (ctx.value as number) * 2 }; + }); + + const result = await contextFn({}); + + assert.deepEqual(executionOrder, ['first', 'second', 'third'], 'middleware executed in correct order'); + assert.equal(result.value, 4, 'middleware transformations applied correctly: (1 + 1) * 2 = 4'); + assert.end(); + }); + + t.test('Context Building: Factory Execution', async (assert) => { + let factoryCallCount = 0; + + const component = new GraphQLComponent({ + types: `type Query { test: String }`, + context: { + namespace: 'test', + factory: async () => { + factoryCallCount++; + return { staticValue: 'cached' }; + } + } + }); + + // Multiple context builds should not re-call factory for static parts + const context1 = await component.context({ requestId: 'req1' }); + const context2 = await component.context({ requestId: 'req2' }); + + assert.equal(context1.test.staticValue, 'cached', 'context 1 has correct value'); + assert.equal(context2.test.staticValue, 'cached', 'context 2 has correct value'); + assert.equal(factoryCallCount, 2, 'factory called for each dynamic context build'); + assert.end(); + }); + + t.test('Integration: Basic Request Processing', async (assert) => { + const component = new GraphQLComponent({ + types: ` + type Query { + hello(name: String!): String + } + `, + resolvers: { + Query: { + hello(_, { name }) { + return `Hello, ${name}!`; + } + } + } + }); + + const schema = component.schema; + const query = `{ hello(name: "World") }`; + + const result = await graphql({ schema, source: query, contextValue: {} }); + + assert.ok(!result.errors, 'request completed without errors'); + const data = result.data as any; + assert.equal(data?.hello, 'Hello, World!', 'request returned correct data'); + assert.end(); + }); + + t.end(); +}); \ No newline at end of file