Skip to content
Merged
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
32 changes: 14 additions & 18 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,21 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Generate GraphQL TypeScript types
run: |
npm run graphql:codegen
git diff --exit-code -- src/graphql/client/generated.ts

- name: Run TypeScript check
run: npx tsc --noEmit

- name: Test GraphQL gateway and cursor pagination
run: >-
npm test -- --runInBand
src/graphql
src/discovery/reviews/agent-reviews.service.spec.ts
src/common/pagination/cursor-pagination.service.spec.ts

- name: Build project
run: npm run build

Expand Down Expand Up @@ -147,21 +159,5 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: typescript-client
path: sdks/typescript/
retention-days: 90

- name: Upload Python client artifact
uses: actions/upload-artifact@v4
with:
name: python-client
path: sdks/python/
retention-days: 90

- name: Upload SDK examples
uses: actions/upload-artifact@v4
with:
name: sdk-examples
path: |
examples/typescript-client-example.ts
examples/python-client-example.py
retention-days: 90
path: client/
retention-days: 30
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ docs/openapi.json

# Generated TypeScript client — produced by `npm run openapi:client`
client/
!src/graphql/client/
!src/graphql/client/**

# OpenAPI Generator CLI cache
.openapi-generator/
Expand Down
14 changes: 1 addition & 13 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,13 +1 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
{}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ See [SECURITY.md](SECURITY.md) for vulnerability reporting details.

## API Endpoints

### GraphQL Gateway

The authenticated GraphQL endpoint is `POST /api/v1/graphql`. It provides
typed, cursor-paginated approved agent reviews and rating summaries without
replacing the existing REST endpoints. See
[GraphQL Gateway](docs/GRAPHQL_GATEWAY.md) for the schema, pagination contract,
type generation, and typed client example.

### Authentication

The backend supports two authentication methods:
Expand Down
23 changes: 23 additions & 0 deletions codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
schema: "src/graphql/schema.graphql",
documents: "src/graphql/client/operations.graphql",
generates: {
"src/graphql/client/generated.ts": {
plugins: ["typescript", "typescript-operations", "typed-document-node"],
config: {
enumsAsTypes: true,
immutableTypes: true,
scalars: {
DateTime: {
input: "string",
output: "string",
},
},
},
},
},
};

export default config;
135 changes: 135 additions & 0 deletions docs/GRAPHQL_GATEWAY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# GraphQL Gateway

The GraphQL gateway is a typed frontend-facing layer over existing NestJS
services. It does not replace the REST API or duplicate review business logic.

```text
TypeScript client -> authenticated Nest HTTP controller -> GraphQL execution
-> AgentReviewsService
-> TypeORM
-> request-local author DataLoader
```

The gateway uses the reference `graphql` implementation behind a normal Nest
controller, so it follows the application's existing HTTP lifecycle without
adding a second web server or an Apollo-specific runtime.

## Endpoint and authentication

Send GraphQL requests to:

```text
POST /api/v1/graphql
Authorization: Bearer <access-token>
Content-Type: application/json
```

The endpoint uses the same global strategy authentication, quota, role, and
KYC guards as REST. Schema introspection is disabled when
`NODE_ENV=production`. Only approved reviews and safe author fields (`id` and
`username`) are exposed; passwords, tokens, moderation details, and other
credentials are absent from the schema.

## Queries

- `agentReviews(agentId: ID!, first: Int! = 20, after: String)` returns
approved reviews in a connection.
- `agentRating(agentId: ID!)` returns the approved review average, count, and
1-5 star distribution.

Example:

```graphql
query AgentReviews($agentId: ID!, $first: Int!, $after: String) {
agentReviews(agentId: $agentId, first: $first, after: $after) {
edges {
cursor
node {
id
rating
reviewText
createdAt
author {
id
username
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
```

## Pagination

The first page omits `after`. Page size `first` must be an integer from 1 to 50. Results use stable keyset ordering:

```text
createdAt DESC, id DESC
```

The UUID tiebreaker prevents duplicate or skipped records when reviews have
the same creation timestamp. A composite database index covers the agent,
approval status, timestamp, and ID traversal. The service reads one extra row
to calculate `hasNextPage`, avoiding a count query. Cursors are URL-safe,
versioned, and opaque: clients must store and return them unchanged and must
not decode, construct, or infer meaning from them. Malformed, non-canonical,
unsupported, or stale-format cursors return a bad-request GraphQL error.

To fetch another page, pass the previous `pageInfo.endCursor` as `after` only
when `pageInfo.hasNextPage` is true. Empty and final pages return
`hasNextPage: false`; an empty page has null start and end cursors.

## DataLoader batching

The `AgentReview.author` relationship uses one DataLoader instance per request.
All author IDs selected in a query are resolved through one
`UserService.findManyByIds` lookup, and duplicate IDs use the request-local
cache. A new loader and cache are created for every request, preventing data
from leaking between clients or authenticated users.

## Generated TypeScript types

The schema is [schema.graphql](../src/graphql/schema.graphql), and frontend
operations are in
[operations.graphql](../src/graphql/client/operations.graphql). Generate the
typed result/variable definitions and `TypedDocumentNode` values with:

```bash
npm run graphql:codegen
```

The checked-in output is
[generated.ts](../src/graphql/client/generated.ts). Frontends can import the
generated documents, operation types, and example helpers through the
[client entry point](../src/graphql/client/index.ts). CI regenerates the types,
fails if the committed SDK is stale, and runs the gateway and pagination tests.

## Typed client example

[example.ts](../src/graphql/client/example.ts) uses the generated documents and
operation types with the platform `fetch` API. `loadAgentReviews` demonstrates
safe field access, reading `endCursor`, checking `hasNextPage`, and loading the
next page. `loadAgentRating` returns a fully typed rating summary, while the
exported `executeGraphql` helper supports additional generated operations
without adding a frontend framework or GraphQL client dependency.

## Development and testing

```bash
npm install
npm run graphql:codegen
npx tsc --noEmit
npm test
npm run build
```

GraphQL unit, execution, and HTTP contract tests cover cursor validation,
stable ordering, first/next/final/empty pages, argument limits, query execution,
author batching, request cache isolation, authentication, and the documented
`/api/v1/graphql` route.
4 changes: 3 additions & 1 deletion nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"entryFile": "main",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true
"webpack": true,
"assets": ["graphql/schema.graphql"],
"watchAssets": true
}
}
Loading
Loading