Initial Commit
This commit is contained in:
86
openspec/changes/enterprise-api-plugin-graphql/proposal.md
Normal file
86
openspec/changes/enterprise-api-plugin-graphql/proposal.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# OpenSpec Change: enterprise-api-plugin-graphql
|
||||
|
||||
- **Change ID**: enterprise-api-plugin-graphql
|
||||
- **Package**: `@suaveplan/api-plugin-graphql`
|
||||
- **Category**: api/data
|
||||
- **Target path**: `api/data/api-plugin-graphql`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/api-plugin-graphql.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/api-plugin-graphql` on the genesis branch from scratch at enterprise quality. GraphQL plugin for `@suaveplan/api`: schema-first with type-safe resolvers, query depth and complexity limiting, persisted queries, DataLoader-based N+1 prevention, subscription support over SSE/WebSocket, query cost analysis, auth integration (`@context.user`), per-operation rate limiting, and an introspection toggle (disabled in production). Routes: `POST /graphql`, `GET /graphql` (GraphiQL in dev). Built on `graphql-js` with a lightweight schema-builder abstraction. Events: `graphql:operation:completed`, `graphql:error`.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/api` (`workspace:^`) | `definePlugin` |
|
||||
| `@suaveplan/api-shared` (`workspace:^`) | Helpers |
|
||||
| `@suaveplan/api-plugin-auth` (`workspace:^`) | `requireAuth` |
|
||||
| `@suaveplan/events` (`workspace:^`) | Event bus (subscriptions fan-out) |
|
||||
| `@suaveplan/rate-limiter` (`workspace:^`) | Per-operation rate limits |
|
||||
| `@suaveplan/error` (`workspace:^`) | `GraphqlPluginError` hierarchy |
|
||||
| `graphql` (`catalog:server`) | GraphQL execution engine |
|
||||
| `hono` + `zod` | Routes/schemas |
|
||||
| `@opentelemetry/api` | OTel |
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
```ts
|
||||
function createGraphqlPlugin(schema: GraphQLSchema, config?: GraphqlPluginConfig): PluginDescriptor
|
||||
|
||||
interface GraphqlPluginConfig {
|
||||
path?: string // default: /graphql
|
||||
introspection?: boolean // default: false in production
|
||||
maxDepth?: number
|
||||
maxComplexity?: number
|
||||
persistedQueries?: PersistedQueryStore
|
||||
playground?: boolean
|
||||
}
|
||||
|
||||
// Helper for building typed resolvers:
|
||||
function createResolver<TRoot, TArgs, TContext, TResult>(
|
||||
fn: (root: TRoot, args: TArgs, ctx: TContext) => Promise<TResult>
|
||||
): typeof fn
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type GraphqlPluginErrorCode =
|
||||
| "GRAPHQL_QUERY_TOO_DEEP"
|
||||
| "GRAPHQL_QUERY_TOO_COMPLEX"
|
||||
| "GRAPHQL_PERSISTED_QUERY_NOT_FOUND"
|
||||
| "GRAPHQL_EXECUTION_ERROR"
|
||||
| "GRAPHQL_PARSE_ERROR"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
api/data/api-plugin-graphql/
|
||||
package.json tsconfig.json vitest.config.ts
|
||||
src/
|
||||
index.ts events.ts types/ errors/
|
||||
core/depth-limiter/ core/complexity/ core/dataloader/ core/persisted-queries/
|
||||
middleware/auth-context/
|
||||
routes/graphql/
|
||||
utils/schema-helpers.ts
|
||||
tests/integration.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
Prerequisites: `@suaveplan/api`, `@suaveplan/api-plugin-auth`, `@suaveplan/events`, `@suaveplan/rate-limiter`, `graphql`.
|
||||
73
openspec/changes/enterprise-api-plugin-graphql/tasks.md
Normal file
73
openspec/changes/enterprise-api-plugin-graphql/tasks.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Tasks: enterprise-api-plugin-graphql
|
||||
|
||||
**Change ID**: enterprise-api-plugin-graphql
|
||||
**Package**: `@suaveplan/api-plugin-graphql`
|
||||
**Target path**: `api/data/api-plugin-graphql`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/api`, `@suaveplan/api-shared`, `@suaveplan/api-plugin-auth`, `@suaveplan/events`, `@suaveplan/rate-limiter`, `@suaveplan/error`, `graphql` (external)
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold + types + errors + events
|
||||
|
||||
- [ ] Create directory; `package.json` v0.0.0; `graphql` in deps (catalog); standard tooling.
|
||||
- [ ] Types: `GraphqlPluginConfig` (Zod), `PersistedQueryStore`, `GraphqlContext`, `OperationResult`.
|
||||
- [ ] Errors: 5-code union + registration. Test + doc.
|
||||
- [ ] Events: `GraphqlEventMap` (graphql:operation:completed, graphql:error).
|
||||
|
||||
---
|
||||
|
||||
## 2. Core: depth limiter
|
||||
|
||||
- [ ] `buildMaxDepthRule(maxDepth: number): ValidationRule` — GraphQL validation rule that rejects queries deeper than `maxDepth`. Test + doc.
|
||||
|
||||
---
|
||||
|
||||
## 3. Core: complexity analysis
|
||||
|
||||
- [ ] `buildComplexityRule(maxComplexity: number, fieldComplexities?: Record<string, number>): ValidationRule` — scores query and rejects over threshold. Test + doc.
|
||||
|
||||
---
|
||||
|
||||
## 4. Core: DataLoader factory
|
||||
|
||||
- [ ] `createDataLoader<K, V>(batchFn)` — thin wrapper ensuring all resolvers use DataLoader for N+1 prevention. Test + doc.
|
||||
|
||||
---
|
||||
|
||||
## 5. Core: persisted queries
|
||||
|
||||
- [ ] `PersistedQueryStore` interface: `get(hash: string): Promise<string | null>`, `set(hash, query): Promise<void>`.
|
||||
- [ ] `MemoryPersistedQueryStore` default. Test + doc.
|
||||
|
||||
---
|
||||
|
||||
## 6. Auth context middleware
|
||||
|
||||
- [ ] Extract JWT user from `requireAuth`; make available as `ctx.user` in GraphQL execution context.
|
||||
- [ ] Test + doc.
|
||||
|
||||
---
|
||||
|
||||
## 7. Routes
|
||||
|
||||
- [ ] `POST /graphql` — parse body; apply persisted-query lookup if `extensions.persistedQuery` present; validate (depth + complexity + auth); execute; record OTel span + counters; emit event.
|
||||
- [ ] `GET /graphql` — serve GraphiQL HTML if `config.playground` and `NODE_ENV !== 'production'`.
|
||||
- [ ] Rate limit per operation type on `POST /graphql`.
|
||||
- [ ] Route `.test.ts` + `.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Plugin factory + integration tests + publish
|
||||
|
||||
- [ ] `createGraphqlPlugin(schema, config?)`: `definePlugin`, `dependencies: ["auth"]`.
|
||||
- [ ] `onInit`: register configured middleware; disable introspection in production.
|
||||
- [ ] Integration: execute simple query → depth limit rejected → persisted query found + executed.
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin-graphql` — green; 100% coverage.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/api-plugin-graphql`.
|
||||
- [ ] Archive → `openspec/archive/2026-04-24-enterprise-api-plugin-graphql/`.
|
||||
Reference in New Issue
Block a user