Skip to content

Patch 602 #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions .cursor/rules/components.mdc
Original file line number Diff line number Diff line change
@@ -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);
}
}
};
```
166 changes: 166 additions & 0 deletions .cursor/rules/datasources.mdc
Original file line number Diff line number Diff line change
@@ -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<T>` and `IDataSource`
- Include `name` property (string) for identification
- Context parameter MUST be first in all methods

```typescript
class MyDataSource implements DataSourceDefinition<MyDataSource>, 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<MyDataSource>`
- 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<UserDataSource>, IDataSource {
name = 'users';

async getUser(context: ComponentContext, id: string): Promise<User> {
// 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
Loading