graphql-component
Version:
Build, customize and compose GraphQL schemas in a componentized fashion
304 lines (231 loc) • 8.31 kB
Markdown
# GraphQL Component

A library for building modular and composable GraphQL schemas through a component-based architecture.
## Overview
`graphql-component` enables you to build GraphQL schemas progressively through a tree of components. Each component encapsulates its own schema, resolvers, and data sources, making it easier to build and maintain large GraphQL APIs.
Read more about the architecture principles in our [blog post](https://medium.com/expedia-group-tech/graphql-component-architecture-principles-homeaway-ede8a58d6fde).
## Features
- 🔧 **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
- 🛠️ **Flexible Configuration**: Extensive options for schema customization
## Installation
```bash
npm install graphql-component
```
## Quick Start
```javascript
const GraphQLComponent = require('graphql-component');
const { schema, context } = new GraphQLComponent({
types,
resolvers
});
```
## Core Concepts
### Schema Construction
A `GraphQLComponent` instance creates a GraphQL schema in one of two ways:
1. **With Imports**: Creates a gateway/aggregate schema by combining imported component schemas with local types/resolvers
2. **Without Imports**: Uses `makeExecutableSchema()` to generate a schema from local types/resolvers
### Federation Support
To create Apollo Federation subgraphs, set `federation: true` in the component options:
```javascript
const component = new GraphQLComponent({
types,
resolvers,
federation: true
});
```
This uses `@apollo/federation`'s `buildSubgraphSchema()` instead of `makeExecutableSchema()`.
## API Reference
### GraphQLComponent Constructor
```typescript
new GraphQLComponent(options: IGraphQLComponentOptions)
```
#### Options
- `types`: `string | string[]` - GraphQL SDL type definitions
- `resolvers`: `object` - Resolver map for the schema
- `imports`: `Array<Component | ConfigObject>` - Components to import
- `context`: `{ namespace: string, factory: Function }` - Context configuration
- `mocks`: `boolean | object` - Enable default or custom mocks
- `dataSources`: `Array<DataSource>` - Data source instances
- `dataSourceOverrides`: `Array<DataSource>` - Override default data sources
- `federation`: `boolean` - Enable Apollo Federation support (default: `false`)
- `pruneSchema`: `boolean` - Enable schema pruning (default: `false`)
- `pruneSchemaOptions`: `object` - Schema pruning options
- `transforms`: `Array<Transform>` - Schema transformation functions
### Component Instance Properties
```typescript
interface IGraphQLComponent {
readonly name: string;
readonly schema: GraphQLSchema;
readonly context: IContextWrapper;
readonly types: TypeSource;
readonly resolvers: IResolvers<any, any>;
readonly imports?: (IGraphQLComponent | IGraphQLComponentConfigObject)[];
readonly dataSources?: IDataSource[];
readonly dataSourceOverrides?: IDataSource[];
federation?: boolean;
}
```
## Usage Examples
### Component Extension
```javascript
class PropertyComponent extends GraphQLComponent {
constructor(options) {
super({
types,
resolvers,
...options
});
}
}
```
### Schema Aggregation
```javascript
const { schema, context } = new GraphQLComponent({
imports: [
new PropertyComponent(),
new ReviewsComponent()
]
});
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:
```typescript
// When implementing a data source:
class MyDataSource implements DataSourceDefinition<MyDataSource> {
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<T>`: Interface for implementing data sources - methods must accept context as first parameter
- `DataSource<T>`: 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,
DataSourceDefinition,
ComponentContext
} from 'graphql-component';
// Define your data source with proper types
class UsersDataSource implements DataSourceDefinition<UsersDataSource> {
name = 'users';
// Static property
defaultRole = 'user';
// Context is required as first parameter when implementing
async getUserById(context: ComponentContext, id: string): Promise<User> {
// 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<User[]> {
// Implementation details...
return [
{ id: '1', name: 'User 1', role },
{ id: '2', name: 'User 2', role }
];
}
}
// In resolvers, the context is automatically injected
const resolvers = {
Query: {
user: (_, { id }, context) => {
// No need to pass context - it's injected by the proxy
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]
}
`,
resolvers,
dataSources: [new UsersDataSource()]
});
```
#### Data Source Overrides
You can override data sources when needed (for testing or extending functionality). The override must follow the same interface:
```typescript
// For testing - create a mock data source
class MockUsersDataSource implements DataSourceDefinition<UsersDataSource> {
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 }];
}
}
// Use the component with overrides
const testComponent = new GraphQLComponent({
imports: [usersComponent],
dataSourceOverrides: [new MockUsersDataSource()]
});
// 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' }
```
## Examples
The repository includes example implementations:
### Local Schema Composition
```bash
npm run start-composition
```
### Federation Example
```bash
npm run start-federation
```
Both examples are accessible at `http://localhost:4000/graphql`
## Debugging
Enable debug logging with:
```bash
DEBUG=graphql-component:* node your-app.js
```
## Repository Structure
- `src/` - Core library code
- `examples/`
- `composition/` - Schema composition example
- `federation/` - Federation implementation example
## Contributing
Please read our contributing guidelines (link) for details on our code of conduct and development process.
## License
This project is licensed under the MIT License - see the LICENSE file for details.