|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guida operativa per chi sviluppa su questo repository. |
| 4 | + |
| 5 | +## Cos'è |
| 6 | + |
| 7 | +Directus endpoint extension che espone Swagger UI (`/api-docs`) e OpenAPI (`/api-docs/oas`) mergiando lo spec core di Directus con le definizioni custom delle altre extension. Le definizioni custom possono essere dichiarate in due modi, supportati nello stesso progetto: |
| 8 | + |
| 9 | +1. **YAML** — `oasconfig.yaml` + `oas.yaml` per extension; validazione runtime opzionale tramite `validate(router, services, schema, paths?)` che monta `express-openapi-validator`. |
| 10 | +2. **Zod** — `defineEndpoint(id, (route, ctx) => { route({...}) })` è l'API ergonomica: deriva il prefix OpenAPI da `id`, espone `services`/`getSchema` nello scope e wrappa internamente il `defineEndpoint` del SDK. `defineRoute(router, {...})` resta come API low-level per casi misti Zod+Express raw. Tutto esposto come named export del main, accanto a `validate`: `import { defineEndpoint, defineRoute, registerSchema, z } from 'directus-extension-api-docs'`. |
| 11 | + |
| 12 | +Le due strade si fondono in `src/index.ts`, route handler `GET /oas`: core spec → merge YAML (`config.paths/tags/components`) → merge `buildZodOasFragment()` → eventuale `filterPaths(publishedTags)`. |
| 13 | + |
| 14 | +## Comandi (sempre `pnpm`) |
| 15 | + |
| 16 | +``` |
| 17 | +pnpm install |
| 18 | +pnpm test # Jest |
| 19 | +pnpm typecheck # tsc --noEmit (richiesto per i type-test in tests/zod/types.test-d.ts) |
| 20 | +pnpm lint # eslint |
| 21 | +pnpm build # directus-extension build → singolo dist/index.js |
| 22 | +pnpm dev # build watch |
| 23 | +``` |
| 24 | + |
| 25 | +`pnpm test`, `pnpm typecheck`, `pnpm lint` devono passare puliti prima di committare. |
| 26 | + |
| 27 | +## Architettura sorgenti |
| 28 | + |
| 29 | +``` |
| 30 | +src/ |
| 31 | +├── index.ts Entry point Directus. Esporta { id, validate, handler }. |
| 32 | +│ Handler /oas mergia core + YAML + fragment Zod in quest'ordine. |
| 33 | +├── utils.ts getConfig (scan YAML), getOas/getOasAll, merge(), filterPaths(), getPackage(). |
| 34 | +├── types.ts Tipi YAML (oasConfig, oas). |
| 35 | +└── zod/ Sotto-modulo: i suoi simboli sono ri-esportati come named exports da src/index.ts. |
| 36 | + ├── index.ts Barrel: esegue extendZodWithOpenApi(z); export pubblico. |
| 37 | + ├── registry.ts Singleton OpenAPIRegistry; registerSchema; _resetRegistry (test only). |
| 38 | + ├── validate.ts zodValidator middleware: safeParse params→query→body, envelope errori 400. |
| 39 | + ├── openapi.ts buildZodOasFragment(): registry → {paths, components, tags}. |
| 40 | + ├── route.ts defineRoute(): registry.registerPath + montaggio middleware su router Express. |
| 41 | + └── endpoint.ts defineEndpoint(id, setup): wrapper che ritorna {id, handler}, cura prefix `/<id>`, |
| 42 | + ed espone `route()` curried su router+prefix dentro lo setup callback. |
| 43 | +``` |
| 44 | + |
| 45 | +Riusa `merge()` di `utils.ts` e `filterPaths()` di `utils.ts`. Non reinventare deep-merge o filtro tag. |
| 46 | + |
| 47 | +## Gotcha (cose non ovvie che bruciano tempo) |
| 48 | + |
| 49 | +- **Build single-file.** `directus-extension build` (rollup del SDK) bundla `src/index.ts` con tutti gli import locali (incluso `src/zod/*`) in un unico `dist/index.js`. Le dipendenze npm (`zod`, `@asteasolutions/zod-to-openapi`, `swagger-ui-express`, ...) sono richieste a runtime. Non aggiungere step di build separati per emettere `dist/zod/*` — i simboli Zod sono esposti come named exports di `src/index.ts`. |
| 50 | +- **`@directus/extensions-sdk` è ESM.** Jest+ts-jest non lo carica in contesto CommonJS. I test che caricano `src/index.ts` devono mockarlo: `jest.mock('@directus/extensions-sdk', () => ({ defineEndpoint: (h: unknown) => h }))`. |
| 51 | +- **`getConfig()` legge da `process.cwd()` al module-load di `src/index.ts`.** Per testare diverse fixture YAML, montare `jest.spyOn(process, 'cwd')` **prima** del primo `require('../../src/index')`. ESM `import` viene hoistato e rompe l'ordine — usare `require()` esplicito al module level del test. |
| 52 | +- **Singleton `OpenAPIRegistry`.** `registry.definitions` è un **getter** che ritorna un nuovo array `[...parents, ..._definitions]`; mutarlo non resetta lo stato. `_resetRegistry()` muta `_definitions` (campo privato). |
| 53 | +- **Zod versione.** Direct dep `zod ^3.x`. `@directus/extensions-sdk` trascina transitivamente `zod@4` con `.d.cts` che richiedono TS ≥ 5 per essere parsati — devDep TypeScript `^5.x` è obbligatoria. Non passare a `zod` 4 sul proprio: `@asteasolutions/zod-to-openapi` 7.x supporta solo Zod 3. |
| 54 | +- **Envelope errori comune.** Sia `validate()` (via `express-openapi-validator`) sia `zodValidator` rispondono `400 { message, errors:[{path, message, code}] }`. `path` ha forma `/body/field`, `/params/id`, `/query/limit`. Cambiare la shape è breaking — modificare in coppia entrambi i percorsi e i test. |
| 55 | +- **`useAuthentication`.** Quando `false` (default), il handler `/oas` forza `accountability = { admin: true }` indipendentemente da `req.accountability`. Per testare il gate sull'iniezione path custom serve fixture con `useAuthentication: true`. |
| 56 | + |
| 57 | +## Test |
| 58 | + |
| 59 | +``` |
| 60 | +tests/ |
| 61 | +├── index.test.ts scan YAML, getConfig, merge, filterPaths, bundle support |
| 62 | +├── zod/ |
| 63 | +│ ├── registry.test.ts singleton, .openapi(), reset, complex Zod, extend |
| 64 | +│ ├── validate.test.ts happy paths, error envelope, coercion |
| 65 | +│ ├── route.test.ts tutti i verbi (test.each), opzioni, errori sync/async |
| 66 | +│ ├── openapi.test.ts shape, ogni feature Zod, validazione OAS 3.0 |
| 67 | +│ ├── integration.test.ts merge YAML+Zod, filterPaths su Zod, dedup tag, registry vuoto |
| 68 | +│ └── types.test-d.ts compile-time (verificato da tsc --noEmit), include @ts-expect-error |
| 69 | +└── legacy/ |
| 70 | + ├── validate.test.ts regressione express-openapi-validator (body, query, paths arg, envelope) |
| 71 | + └── oas-handler.test.ts regressione /oas (merge YAML, info, accountability gate, contributi Zod) |
| 72 | +
|
| 73 | +tests/mocks/ |
| 74 | +├── oasconfig/, customoas/, merge/, bundle/, mixed/ fixture YAML |
| 75 | +├── zod-mixed/ fixture YAML+Zod per integration.test.ts |
| 76 | +├── legacy-validate/ fixture per tests/legacy/validate.test.ts |
| 77 | +└── legacy-oas/ fixture per tests/legacy/oas-handler.test.ts (useAuthentication: true) |
| 78 | +``` |
| 79 | + |
| 80 | +I test usano `supertest` + Express in-process; nessun boot di Directus. |
| 81 | + |
| 82 | +## Playground (test runtime in Directus reale) |
| 83 | + |
| 84 | +``` |
| 85 | +playground/ |
| 86 | +├── docker-compose.yml Directus 11 + SQLite, bind-mount del dist/ del repo |
| 87 | +├── .gitignore |
| 88 | +└── extensions/ Mappato su /directus/extensions |
| 89 | + ├── oasconfig.yaml Config YAML root + securitySchemes per il lock in Swagger |
| 90 | + ├── yaml-demo/ POST /echo, GET /users/:id (path param + 200/404), DELETE /items/:id |
| 91 | + │ ├── package.json |
| 92 | + │ ├── index.js |
| 93 | + │ └── oas.yaml |
| 94 | + ├── zod-demo/ 8 rotte: GET/POST/PUT/DELETE, discriminatedUnion, security, deprecated, throw |
| 95 | + │ ├── package.json |
| 96 | + │ └── index.js |
| 97 | + └── directus-services-demo/ Rotte Zod che chiamano UsersService (DB SQLite reale) |
| 98 | + ├── package.json |
| 99 | + └── index.js |
| 100 | +``` |
| 101 | + |
| 102 | +Uso (dalla root del repo): |
| 103 | +``` |
| 104 | +pnpm build # produce dist/index.js |
| 105 | +docker compose -f playground/docker-compose.yml up # boot Directus su :8055 |
| 106 | +``` |
| 107 | + |
| 108 | +L'image è pinnata a `directus/directus:11.17.4` (current stable 11.x — Directus 12 non è ancora rilasciato; 10.x è in ESU). |
| 109 | +Poi `http://localhost:8055/api-docs` (Swagger), `http://localhost:8055/api-docs/oas` (spec), e prova le rotte demo. `EXTENSIONS_AUTO_RELOAD=true` rilegge dist senza restart del container — basta rifare `pnpm build`. Modifiche a `playground/extensions/*/index.js` o `oas.yaml` non richiedono build, vengono prese al volo. |
| 110 | + |
| 111 | +I demo importano `directus-extension-api-docs` via il bind-mount di `package.json` + `dist/` su `extensions/node_modules/directus-extension-api-docs/` dentro il container; nessun `npm install` lato playground è necessario. |
| 112 | + |
| 113 | +## Convenzioni |
| 114 | + |
| 115 | +- Niente nuovi file Markdown a meno che esplicitamente richiesto. |
| 116 | +- Aggiornamenti al README in modo misurato: la sezione "Zod-first routes (optional)" sta in fondo, non sostituisce niente, non marca YAML come deprecato. |
| 117 | +- `noUnusedLocals` / `noUncheckedIndexedAccess` attivi in tsconfig: usare `?.`, `??`, e prefisso `_` per parametri non usati. |
| 118 | +- Commit message: stile esistente del repo (`feat:`, `fix:`, `docs:`, ...). |
0 commit comments