Skip to content

Commit c1583d0

Browse files
authored
fix(query-graphql): add @parent() decorator to resolveReference for Federation #410 (#414)
## Summary Fixes #410 - Federation `referenceBy` broken since v9.2.0 ## Problem The auto-generated `@ResolveReference()` method fails in Apollo Federation because the `representation` parameter is `undefined`. This regression was introduced in v9.2.0 when DataLoader support was added with `@Context()` and `@InjectDataLoaderConfig()` parameter decorators. **Root Cause:** `@nestjs/graphql`'s `@ResolveReference()` decorator has known incompatibilities with parameter decorators (see NestJS issues [#945](nestjs/graphql#945), [#2127](nestjs/graphql#2127)). When parameter decorators are present without a decorator on the first parameter, it becomes `undefined`. ## Solution Add `@Parent()` decorator to the `representation` parameter in `resolveReference()` method: ```typescript // Before (broken) async resolveReference( representation: RepresentationType, @context() context: ExecutionContext, ... ) // After (fixed) async resolveReference( @parent() representation: RepresentationType, @context() context: ExecutionContext, ... ) ``` Also fixed a type consistency issue in `ReferenceLoader` where Map keys needed `String()` conversion for consistent lookups. ## Changes 1. **packages/query-graphql/src/resolvers/reference.resolver.ts** - Added `@Parent()` decorator to `representation` parameter 2. **packages/query-graphql/src/loader/reference.loader.ts** - Changed Map type to `Map<string, DTO>` for consistent key handling - Added `String(id)` conversion for Map operations 3. **packages/query-graphql/__tests__/integration/federation-n1/federation-reference.spec.ts** (NEW) - Added true Federation integration test using `ApolloFederationDriver` - Tests `_entities` query which simulates Gateway requests to subgraph - Validates the fix works correctly ## Testing The new integration test (`federation-reference.spec.ts`) validates: - ✅ TodoList reference resolution via `_entities` query - ✅ TodoItem reference resolution via `_entities` query - ✅ Batch multiple references efficiently (DataLoader) - ✅ Mixed entity types in single `_entities` query - ✅ ID type handling (number vs string) - ✅ Error handling for non-existent entities - ✅ SDL validation with `@key` directive **Verification:** Removing `@Parent()` causes all `_entities` tests to fail with `Cannot read properties of undefined (reading 'id')` - the exact error reported in #410. ## Checklist - [x] Code follows the project's coding standards - [x] Changes have been tested locally - [x] New tests added for the fix - [x] All existing tests pass <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a self-contained Federation V2 end-to-end test environment and a CI job to run it. * **Bug Fixes** * Fixed federation reference resolution when using context/injected data loaders. * Normalized ID handling by coercing IDs to strings for consistent cross-service reference matching. * **Tests** * Added comprehensive E2E and integration tests covering reference resolution, ID formats, batching, and error scenarios. * **Documentation** * Added README describing the Federation V2 E2E setup and quick start. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 5dd2f83 + edf9fc4 commit c1583d0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+33303
-10
lines changed

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,24 @@ jobs:
9595
directory: ./coverage
9696
fail_ci_if_error: false
9797
name: run-e2e-${{ matrix.node-version }}-${{ matrix.db-type }}
98+
99+
federation-e2e-test:
100+
runs-on: ubuntu-latest
101+
steps:
102+
- uses: actions/checkout@v4
103+
with:
104+
fetch-depth: 0
105+
106+
- name: Set up Docker Compose
107+
uses: docker/setup-compose-action@v1
108+
109+
- name: Build and run Federation E2E tests
110+
run: |
111+
cd examples/federation-v2-e2e
112+
docker compose --profile test up --build --exit-code-from e2e-test e2e-test
113+
114+
- name: Show logs on failure
115+
if: failure()
116+
run: |
117+
cd examples/federation-v2-e2e
118+
docker compose logs
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
dist
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!package-lock.json
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Federation V2 E2E Test Environment
2+
3+
E2E test environment for testing [issue #410](https://github.com/TriPSs/nestjs-query/issues/410) fix:
4+
"Federation referenceBy broken since v9.2.0: representation parameter undefined"
5+
6+
## Issue Fixed ✅
7+
8+
This test environment validates the fix for the bug where the `@ResolveReference()` method
9+
was receiving `undefined` for the `representation` parameter when `@Context()` and
10+
`@InjectDataLoaderConfig()` decorators are present.
11+
12+
The fix adds `@Parent()` decorator to properly extract the representation from GraphQL resolver arguments.
13+
14+
## Directory Structure
15+
16+
```text
17+
federation-v2-e2e/
18+
├── docker-compose.yml # Orchestrates all services
19+
├── user-service/ # User subgraph with numeric ID (exposed on host:3001)
20+
│ ├── Dockerfile # Multi-stage build with nestjs-query
21+
│ └── src/
22+
├── tag-service/ # Tag subgraph with UUID string ID (exposed on host:3003)
23+
│ ├── Dockerfile # Multi-stage build with nestjs-query
24+
│ └── src/
25+
├── todo-service/ # TodoItem subgraph (exposed on host:3002)
26+
│ ├── Dockerfile # Multi-stage build with nestjs-query
27+
│ └── src/
28+
├── gateway/ # Apollo Gateway (exposed on host:3000)
29+
│ ├── Dockerfile
30+
│ └── src/
31+
├── e2e-test/ # Jest E2E test suite
32+
│ ├── Dockerfile
33+
│ └── e2e/
34+
└── init-scripts/ # PostgreSQL initialization
35+
```
36+
37+
## ID Type Coverage
38+
39+
This test environment validates Federation with different ID types:
40+
41+
- **Numeric ID**: `User.id` (integer) - Tests standard numeric primary key
42+
- **UUID String ID**: `Tag.id` (UUID) - Tests string-based primary key
43+
44+
## Build Strategy
45+
46+
Each service uses a **multi-stage Docker build**:
47+
48+
1. **Stage 1 (builder)**: Copies the entire nestjs-query source code, installs dependencies,
49+
builds all packages, and creates `.tgz` archives using `npm pack`.
50+
51+
2. **Stage 2 (runtime)**: Copies the packed `.tgz` files, installs them as local dependencies,
52+
then builds and runs the service.
53+
54+
This approach allows testing the **local source code** instead of published npm packages.
55+
56+
## Quick Start
57+
58+
```bash
59+
# From the project root directory
60+
cd examples/federation-v2-e2e
61+
62+
# 1. Build and start all services
63+
docker compose up -d --build
64+
65+
# 2. Wait for services to be healthy (all should show "healthy")
66+
docker compose ps
67+
68+
# 3. Test Federation query with pre-seeded data
69+
curl -s -X POST http://localhost:3000/graphql \
70+
-H "Content-Type: application/json" \
71+
-d '{"query": "{ todoItems { edges { node { id title assignee { id name } tag { id name color } } } } }"}' | jq .
72+
73+
# 4. Cleanup
74+
docker compose down -v
75+
```
76+
77+
## Running E2E Tests
78+
79+
Run the automated Jest test suite:
80+
81+
```bash
82+
# Run tests with the test profile
83+
docker compose --profile test up --build e2e-test
84+
85+
# Or run all services and tests together
86+
docker compose --profile test up --build
87+
```
88+
89+
The test suite validates:
90+
- Federation reference resolution works correctly
91+
- User references with numeric ID are resolved
92+
- Tag references with UUID string ID are resolved
93+
- Issue #410 fix verification
94+
95+
## Expected Result
96+
97+
The query should return TodoItems with resolved User (numeric ID) and Tag (UUID ID) references:
98+
99+
```json
100+
{
101+
"data": {
102+
"todoItems": {
103+
"edges": [
104+
{
105+
"node": {
106+
"id": "1",
107+
"title": "Learn GraphQL Federation",
108+
"assignee": {
109+
"id": "1",
110+
"name": "Alice"
111+
},
112+
"tag": {
113+
"id": "550e8400-e29b-41d4-a716-446655440001",
114+
"name": "Frontend",
115+
"color": "#3498db"
116+
}
117+
}
118+
},
119+
{
120+
"node": {
121+
"id": "2",
122+
"title": "Fix Issue #410",
123+
"assignee": {
124+
"id": "2",
125+
"name": "Bob"
126+
},
127+
"tag": {
128+
"id": "550e8400-e29b-41d4-a716-446655440003",
129+
"name": "Bug",
130+
"color": "#e74c3c"
131+
}
132+
}
133+
},
134+
{
135+
"node": {
136+
"id": "3",
137+
"title": "Write Tests",
138+
"assignee": {
139+
"id": "1",
140+
"name": "Alice"
141+
},
142+
"tag": {
143+
"id": "550e8400-e29b-41d4-a716-446655440002",
144+
"name": "Backend",
145+
"color": "#2ecc71"
146+
}
147+
}
148+
}
149+
]
150+
}
151+
}
152+
}
153+
```
154+
155+
## The Fix
156+
157+
The issue was in `reference.resolver.ts`. The `representation` parameter needed the `@Parent()` decorator:
158+
159+
**Before (broken):**
160+
```typescript
161+
@ResolveReference()
162+
async resolveReference(
163+
representation: RepresentationType, // ❌ No decorator - gets undefined
164+
@Context() context: ExecutionContext,
165+
@InjectDataLoaderConfig() dataLoaderConfig?: DataLoaderOptions
166+
): Promise<DTO>
167+
```
168+
169+
**After (fixed):**
170+
```typescript
171+
@ResolveReference()
172+
async resolveReference(
173+
@Parent() representation: RepresentationType, // ✅ @Parent() extracts from args[0]
174+
@Context() context: ExecutionContext,
175+
@InjectDataLoaderConfig() dataLoaderConfig?: DataLoaderOptions
176+
): Promise<DTO>
177+
```
178+
179+
Additionally, `reference.loader.ts` was updated to handle ID type comparison correctly
180+
by converting IDs to strings for consistent Map lookups.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Federation V2 E2E Test Environment
2+
# Reproduces issue #410: Federation referenceBy broken since v9.2.0
3+
# Tests both numeric ID (User) and UUID string ID (Tag) types
4+
#
5+
# Usage: docker compose up -d --build
6+
# Note: Run from project root directory to access packages/ for building
7+
8+
services:
9+
# PostgreSQL database
10+
postgres:
11+
image: postgres:14
12+
environment:
13+
POSTGRES_USER: postgres
14+
POSTGRES_HOST_AUTH_METHOD: trust
15+
volumes:
16+
- ./init-scripts:/docker-entrypoint-initdb.d/
17+
ports:
18+
- "5436:5432"
19+
healthcheck:
20+
test: ["CMD-SHELL", "pg_isready -U postgres"]
21+
interval: 5s
22+
timeout: 5s
23+
retries: 5
24+
25+
# User subgraph service (numeric ID)
26+
user-service:
27+
build:
28+
context: ../..
29+
dockerfile: examples/federation-v2-e2e/user-service/Dockerfile
30+
ports:
31+
- "3001:3000"
32+
environment:
33+
DB_HOST: postgres
34+
DB_PORT: 5432
35+
DB_USER: federation_user
36+
DB_NAME: federation_user
37+
depends_on:
38+
postgres:
39+
condition: service_healthy
40+
healthcheck:
41+
test: ["CMD", "curl", "-sf", "-X", "POST", "-H", "Content-Type: application/json", "-d", "{\"query\":\"{__typename}\"}", "http://localhost:3000/graphql"]
42+
interval: 10s
43+
timeout: 5s
44+
retries: 10
45+
start_period: 30s
46+
47+
# Tag subgraph service (UUID string ID)
48+
tag-service:
49+
build:
50+
context: ../..
51+
dockerfile: examples/federation-v2-e2e/tag-service/Dockerfile
52+
ports:
53+
- "3003:3000"
54+
environment:
55+
DB_HOST: postgres
56+
DB_PORT: 5432
57+
DB_USER: federation_tag
58+
DB_NAME: federation_tag
59+
depends_on:
60+
postgres:
61+
condition: service_healthy
62+
healthcheck:
63+
test: ["CMD", "curl", "-sf", "-X", "POST", "-H", "Content-Type: application/json", "-d", "{\"query\":\"{__typename}\"}", "http://localhost:3000/graphql"]
64+
interval: 10s
65+
timeout: 5s
66+
retries: 10
67+
start_period: 30s
68+
69+
# TodoItem subgraph service
70+
todo-service:
71+
build:
72+
context: ../..
73+
dockerfile: examples/federation-v2-e2e/todo-service/Dockerfile
74+
ports:
75+
- "3002:3000"
76+
environment:
77+
DB_HOST: postgres
78+
DB_PORT: 5432
79+
DB_USER: federation_todo
80+
DB_NAME: federation_todo
81+
depends_on:
82+
postgres:
83+
condition: service_healthy
84+
healthcheck:
85+
test: ["CMD", "curl", "-sf", "-X", "POST", "-H", "Content-Type: application/json", "-d", "{\"query\":\"{__typename}\"}", "http://localhost:3000/graphql"]
86+
interval: 10s
87+
timeout: 5s
88+
retries: 10
89+
start_period: 30s
90+
91+
# Apollo Gateway
92+
gateway:
93+
build:
94+
context: ../..
95+
dockerfile: examples/federation-v2-e2e/gateway/Dockerfile
96+
ports:
97+
- "3000:3000"
98+
environment:
99+
USER_SERVICE_URL: http://user-service:3000/graphql
100+
TODO_SERVICE_URL: http://todo-service:3000/graphql
101+
TAG_SERVICE_URL: http://tag-service:3000/graphql
102+
depends_on:
103+
user-service:
104+
condition: service_healthy
105+
todo-service:
106+
condition: service_healthy
107+
tag-service:
108+
condition: service_healthy
109+
healthcheck:
110+
test: ["CMD", "curl", "-sf", "-X", "POST", "-H", "Content-Type: application/json", "-d", "{\"query\":\"{__typename}\"}", "http://localhost:3000/graphql"]
111+
interval: 10s
112+
timeout: 5s
113+
retries: 10
114+
start_period: 30s
115+
116+
# E2E Test Runner
117+
e2e-test:
118+
build:
119+
context: ../..
120+
dockerfile: examples/federation-v2-e2e/e2e-test/Dockerfile
121+
environment:
122+
GATEWAY_URL: http://gateway:3000/graphql
123+
depends_on:
124+
gateway:
125+
condition: service_healthy
126+
profiles:
127+
- test
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM node:22
2+
3+
WORKDIR /app
4+
5+
COPY examples/federation-v2-e2e/e2e-test/package*.json ./
6+
RUN npm install
7+
8+
COPY examples/federation-v2-e2e/e2e-test/jest.config.js ./
9+
COPY examples/federation-v2-e2e/e2e-test/tsconfig.json ./
10+
COPY examples/federation-v2-e2e/e2e-test/e2e ./e2e
11+
12+
CMD ["npm", "test"]

0 commit comments

Comments
 (0)