@warlock.js/core 5.26.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/esm/cli/commands/openapi-generator.action.mjs +43 -0
  3. package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
  4. package/esm/cli/commands/openapi-generator.command.mjs +44 -0
  5. package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
  6. package/esm/cli/commands/postman-generator.action.mjs +47 -0
  7. package/esm/cli/commands/postman-generator.action.mjs.map +1 -0
  8. package/esm/cli/commands/postman-generator.command.mjs +43 -0
  9. package/esm/cli/commands/postman-generator.command.mjs.map +1 -0
  10. package/esm/cli/framework-cli-commands.mjs +4 -0
  11. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  12. package/esm/dev-server/build-development-openapi-document.mjs +36 -0
  13. package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
  14. package/esm/dev-server/collect-development-api-routes.mjs +27 -0
  15. package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
  16. package/esm/dev-server/dev-logger.mjs +9 -1
  17. package/esm/dev-server/dev-logger.mjs.map +1 -1
  18. package/esm/dev-server/development-server.mjs +6 -3
  19. package/esm/dev-server/development-server.mjs.map +1 -1
  20. package/esm/dev-server/files-orchestrator.mjs +17 -1
  21. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  22. package/esm/dev-server/model-resource-typings.mjs +246 -0
  23. package/esm/dev-server/model-resource-typings.mjs.map +1 -0
  24. package/esm/dev-server/type-generator.mjs +46 -2
  25. package/esm/dev-server/type-generator.mjs.map +1 -1
  26. package/esm/http/index.d.mts +2 -1
  27. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  28. package/esm/http/middleware/inject-request-context.mjs +29 -3
  29. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  30. package/esm/http/serialized.d.mts +114 -0
  31. package/esm/http/serialized.d.mts.map +1 -0
  32. package/esm/index.d.mts +11 -3
  33. package/esm/index.mjs +6 -1
  34. package/esm/openapi/build-openapi-document.d.mts +17 -0
  35. package/esm/openapi/build-openapi-document.d.mts.map +1 -0
  36. package/esm/openapi/build-openapi-document.mjs +124 -0
  37. package/esm/openapi/build-openapi-document.mjs.map +1 -0
  38. package/esm/openapi/cast-schema.mjs +89 -0
  39. package/esm/openapi/cast-schema.mjs.map +1 -0
  40. package/esm/openapi/collect-openapi-document.mjs +40 -0
  41. package/esm/openapi/collect-openapi-document.mjs.map +1 -0
  42. package/esm/openapi/development-openapi-provider.d.mts +28 -0
  43. package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
  44. package/esm/openapi/development-openapi-provider.mjs +35 -0
  45. package/esm/openapi/development-openapi-provider.mjs.map +1 -0
  46. package/esm/openapi/error-responses.mjs +67 -0
  47. package/esm/openapi/error-responses.mjs.map +1 -0
  48. package/esm/openapi/index.d.mts +6 -0
  49. package/esm/openapi/index.mjs +6 -0
  50. package/esm/openapi/openapi-child-output.mjs +57 -0
  51. package/esm/openapi/openapi-child-output.mjs.map +1 -0
  52. package/esm/openapi/openapi-protocol.mjs +30 -0
  53. package/esm/openapi/openapi-protocol.mjs.map +1 -0
  54. package/esm/openapi/paths.mjs +63 -0
  55. package/esm/openapi/paths.mjs.map +1 -0
  56. package/esm/openapi/postman/build-request-body.mjs +86 -0
  57. package/esm/openapi/postman/build-request-body.mjs.map +1 -0
  58. package/esm/openapi/postman/build-request-url.mjs +85 -0
  59. package/esm/openapi/postman/build-request-url.mjs.map +1 -0
  60. package/esm/openapi/postman/build-saved-responses.mjs +60 -0
  61. package/esm/openapi/postman/build-saved-responses.mjs.map +1 -0
  62. package/esm/openapi/postman/example-from-schema.d.mts +17 -0
  63. package/esm/openapi/postman/example-from-schema.d.mts.map +1 -0
  64. package/esm/openapi/postman/example-from-schema.mjs +92 -0
  65. package/esm/openapi/postman/example-from-schema.mjs.map +1 -0
  66. package/esm/openapi/postman/http-status-text.mjs +31 -0
  67. package/esm/openapi/postman/http-status-text.mjs.map +1 -0
  68. package/esm/openapi/postman/index.d.mts +3 -0
  69. package/esm/openapi/postman/openapi-to-postman-collection.d.mts +22 -0
  70. package/esm/openapi/postman/openapi-to-postman-collection.d.mts.map +1 -0
  71. package/esm/openapi/postman/openapi-to-postman-collection.mjs +109 -0
  72. package/esm/openapi/postman/openapi-to-postman-collection.mjs.map +1 -0
  73. package/esm/openapi/postman/postman-types.d.mts +119 -0
  74. package/esm/openapi/postman/postman-types.d.mts.map +1 -0
  75. package/esm/openapi/postman/resolve-operation-auth.mjs +51 -0
  76. package/esm/openapi/postman/resolve-operation-auth.mjs.map +1 -0
  77. package/esm/openapi/postman/schema-refs.d.mts +7 -0
  78. package/esm/openapi/postman/schema-refs.d.mts.map +1 -0
  79. package/esm/openapi/postman/schema-refs.mjs +41 -0
  80. package/esm/openapi/postman/schema-refs.mjs.map +1 -0
  81. package/esm/openapi/postman/uuid-v5.mjs +29 -0
  82. package/esm/openapi/postman/uuid-v5.mjs.map +1 -0
  83. package/esm/openapi/request-schemas.mjs +138 -0
  84. package/esm/openapi/request-schemas.mjs.map +1 -0
  85. package/esm/openapi/response-schemas.mjs +85 -0
  86. package/esm/openapi/response-schemas.mjs.map +1 -0
  87. package/esm/openapi/schema-registry.mjs +143 -0
  88. package/esm/openapi/schema-registry.mjs.map +1 -0
  89. package/esm/openapi/security.mjs +98 -0
  90. package/esm/openapi/security.mjs.map +1 -0
  91. package/esm/openapi/types.d.mts +119 -0
  92. package/esm/openapi/types.d.mts.map +1 -0
  93. package/esm/production/registration-child-runner.mjs +139 -0
  94. package/esm/production/registration-child-runner.mjs.map +1 -0
  95. package/esm/production/route-registration-child.mjs +30 -2
  96. package/esm/production/route-registration-child.mjs.map +1 -1
  97. package/esm/production/route-registration-snapshot.mjs +22 -119
  98. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  99. package/esm/resource/define-resource.d.mts +44 -14
  100. package/esm/resource/define-resource.d.mts.map +1 -1
  101. package/esm/resource/define-resource.mjs +0 -40
  102. package/esm/resource/define-resource.mjs.map +1 -1
  103. package/esm/resource/index.d.mts +1 -1
  104. package/esm/resource/resource.d.mts +4 -4
  105. package/esm/resource/resource.d.mts.map +1 -1
  106. package/esm/resource/resource.mjs +2 -4
  107. package/esm/resource/resource.mjs.map +1 -1
  108. package/esm/resource/types.d.mts +89 -4
  109. package/esm/resource/types.d.mts.map +1 -1
  110. package/esm/router/index.d.mts +1 -1
  111. package/esm/router/named-api-routes-with-responses.mjs +59 -0
  112. package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
  113. package/esm/router/response-schema-types.mjs +100 -0
  114. package/esm/router/response-schema-types.mjs.map +1 -0
  115. package/esm/router/router.d.mts +2 -2
  116. package/esm/router/router.d.mts.map +1 -1
  117. package/esm/router/router.mjs +38 -11
  118. package/esm/router/router.mjs.map +1 -1
  119. package/esm/router/types.d.mts +16 -1
  120. package/esm/router/types.d.mts.map +1 -1
  121. package/llms-full.txt +293 -5
  122. package/llms.txt +3 -2
  123. package/package.json +13 -13
  124. package/skills/README.md +3 -2
  125. package/skills/create-controller/SKILL.md +39 -2
  126. package/skills/define-resource/SKILL.md +100 -1
  127. package/skills/generate-openapi/SKILL.md +148 -0
  128. package/skills/index.md +3 -3
  129. package/skills/validate-input/SKILL.md +2 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: define-resource
3
- description: 'Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.'
3
+ description: 'Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Also covers the typed output: `ResourceOutput<typeof R>`, `defineResource<Out>()`, `Serialized<T, W>` and `ModelResourceRegistry`. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `ResourceOutput`, `Serialized`, `ModelResourceRegistry`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "type a resource''s output", "what does the client receive", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.'
4
4
  ---
5
5
 
6
6
  # Warlock — define a resource
@@ -196,6 +196,8 @@ export const ChatResource = defineResource({
196
196
  transform(data, resource) {
197
197
  // final pass — mutate `data` in place; the return value is ignored
198
198
  data.slug = String(data.title).toLowerCase();
199
+
200
+ return data;
199
201
  },
200
202
  });
201
203
  ```
@@ -204,6 +206,103 @@ export const ChatResource = defineResource({
204
206
 
205
207
  Hooks are an escape hatch. If you find yourself running queries or reconciling state inside a hook, stop — that work belongs in a service or in a `prepare-*` step before the resource is constructed.
206
208
 
209
+ ## Typed output
210
+
211
+ `defineResource()` reads the literal cast strings in `schema` and types `toJSON()` with them. Nothing is annotated by hand.
212
+
213
+ ```ts title="src/app/users/resources/user.resource.ts"
214
+ import { defineResource, type ResourceOutput } from "@warlock.js/core";
215
+
216
+ export const UserResource = defineResource({
217
+ schema: {
218
+ id: "number",
219
+ name: "string?",
220
+ tags: "string[]",
221
+ createdAt: "date",
222
+ },
223
+ });
224
+
225
+ export type UserJson = ResourceOutput<typeof UserResource>;
226
+ // { id: number; name: string | null; tags: string[]; createdAt: { iso: string; format: string; timestamp: number; humanTime: string } }
227
+
228
+ const json = new UserResource(user).toJSON(); // typed as UserJson
229
+ ```
230
+
231
+ `ResourceOutput<R>` takes the resource class or an instance (`InstanceType<typeof UserResource>`).
232
+
233
+ How each schema entry is typed:
234
+
235
+ | Schema entry | Output type |
236
+ | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
237
+ | `"string"`, `"localized"`, `"url"`, `"uploadsUrl"`, `"storageUrl"` | `string` |
238
+ | `"number"`, `"float"`, `"int"` | `number` |
239
+ | `"boolean"` | `boolean` |
240
+ | `"date"` | `{ iso: string; format: string; timestamp: number; humanTime: string }` (the default date output) |
241
+ | `"object"` / `"array"` | `Record<string, unknown>` / `unknown[]` |
242
+ | `"x[]"` | array of the base type |
243
+ | `"x?"` | `T \| null`; the key is always present, never optional |
244
+ | a nested resource, `lazy(() => R)` | that resource's output (one object, even if the model holds an array) |
245
+ | `"self"` / `"self[]"` | this resource's own output / an array of it |
246
+ | `["inputKey", "cast"]` tuple | the cast's type |
247
+ | a resolver function | its return type (an `any` return becomes `unknown`) |
248
+ | a field builder (`this.date()...`, `new ResourceFieldBuilder(...)`) | `unknown` |
249
+
250
+ The type reflects the schema only, not the data: mark a field `?` when the model may not have a value. `boot`, `extend`, `transform` and builders can change the real shape; when they do, state the output with a type argument, which replaces the inferred type:
251
+
252
+ ```ts title="src/app/products/resources/product.resource.ts"
253
+ import { defineResource } from "@warlock.js/core";
254
+
255
+ type ProductJson = { id: number; title: string; priceLabel: string };
256
+
257
+ export const ProductResource = defineResource<ProductJson>({
258
+ schema: { id: "number", title: "string", price: "number" },
259
+ transform: (data) => {
260
+ data.priceLabel = `$${data.price}`;
261
+ delete data.price;
262
+
263
+ return data;
264
+ },
265
+ });
266
+ ```
267
+
268
+ `transform` still mutates `data` in place (see Hooks); the returned value is ignored at runtime, but the option type requires a return. `ProductJson` is what consumers of `ResourceOutput<typeof ProductResource>` see.
269
+
270
+ ### `Serialized<T, W>` — what the client receives
271
+
272
+ `Serialized<T, W>` is the type of a value after Warlock serializes it. `W` is the wire: `"json"` (default) for API responses, `"devalue"` for web page loader data. It is exported from `@warlock.js/core`.
273
+
274
+ ```ts
275
+ import type { Serialized } from "@warlock.js/core";
276
+
277
+ type Payload = Serialized<{ createdAt: Date; note?: string }>;
278
+ // { createdAt: string; note?: string }
279
+ ```
280
+
281
+ Rules, in the order the runtime applies them:
282
+
283
+ - Primitives, `Buffer` and `Uint8Array` pass through. `any` and `unknown` stay as they are.
284
+ - On `"devalue"`, `Date`, `RegExp`, `URL`, `Map` and `Set` stay native. On `"json"`, a `Map` becomes a record and a `Set` an array.
285
+ - A cascade model becomes its resource's output (next section); with no registry entry it becomes its serialized `data` type.
286
+ - Anything with `toJSON()` becomes the (awaited) serialized result, which is how a `Date` becomes a `string` on `"json"`.
287
+ - Arrays map item by item. Functions are dropped from objects.
288
+ - On `"json"`, a key whose value can be `undefined` becomes optional, because `JSON.stringify` omits it.
289
+ - A class with private or protected members and no `toJSON()` maps to its public data on `"json"` and to `never` on `"devalue"` (devalue throws on it at render). `Error` is `{}` on `"json"`.
290
+ - Nesting is walked to a fixed depth (8), then the type is `unknown`.
291
+
292
+ ### `ModelResourceRegistry` — map a model to its resource
293
+
294
+ `Model.resource` is typed loosely, so a model type cannot reveal its resource by itself. You don't write the mapping: `warlock generate.typings` and `warlock dev` generate `.warlock/typings/model-resources.d.ts` from every model that declares `static resource = SomeResource`:
295
+
296
+ ```ts title=".warlock/typings/model-resources.d.ts (generated)"
297
+ declare module "@warlock.js/core" {
298
+ interface ModelResourceRegistry {
299
+ "User": { model: import("../../src/app/users/models/user.model").User; resource: typeof import("../../src/app/users/resources/user.resource").UserResource };
300
+ }
301
+ }
302
+ ```
303
+
304
+ `Serialized<User>` then resolves to `ResourceOutput<typeof UserResource>` instead of the raw model data (so hidden columns such as a password never appear in the type). The generator reads the model file statically and resolves the identifier through its imports (named, aliased or default, or a resource declared in the same file). It skips anything it cannot resolve statically (a computed value, `lazy(...)`, a namespace import): that model keeps its raw data type. Your `tsconfig.json` must include `.warlock/typings/*.d.ts` (new apps do). `@warlock.js/web` uses the same registry to type page loader data.
305
+
207
306
  ## Attaching to a model
208
307
 
209
308
  Models reference their default resource via a static `resource` field. Repositories and other helpers can then serialize without importing the resource:
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: generate-openapi
3
+ description: 'Generate an OpenAPI 3.1 document from the registered routes with `warlock generate.openapi` (default `storage/openapi/openapi.json`), write a Postman collection from it with `warlock generate.postman` (default `storage/postman/collection.json`), and read it live in the dev-only API docs page at `/__warlock/docs`. Documents paths, path/query parameters, request bodies from Seal validation, responses from `handler.responseSchema`, 401/422 and bearer/cookie security from `authMiddleware()`. Triggers: `generate.openapi`, `openApiGeneratorCommand`, `buildOpenApiDocument`, `getDevelopmentOpenApiDocument`, `generate.postman`, `openApiToPostmanCollection`, `getDevelopmentPostmanCollection`, `responseSchema`, `--include-pages`, `/__warlock/docs`; "OpenAPI", "Swagger", "API docs", "import my API into Postman or Insomnia", "document my routes". Skip: declaring response types on a handler — `@warlock.js/core/create-controller/SKILL.md`; the devtools dashboard — `@warlock.js/devtools/devtools-overview/SKILL.md`; listing routes — `@warlock.js/core/warlock-routes/SKILL.md`.'
4
+ ---
5
+
6
+ # Warlock — generate an OpenAPI document
7
+
8
+ `warlock generate.openapi` writes an OpenAPI 3.1 JSON document derived from the routes your app registers. Nothing is annotated by hand: the document reads the route table, each handler's `validation`, `description` and `responseSchema`, and the auth middleware on the route.
9
+
10
+ ```bash
11
+ npx warlock generate.openapi
12
+ # OpenAPI 3.1.0 written to <root>/storage/openapi/openapi.json (12 paths, 17 operations).
13
+ ```
14
+
15
+ Routes are read in the same isolated registration child process `warlock build` uses: your route modules are imported, **no connector starts** (no database, cache or socket), and the running dev server is untouched. If the app cannot be loaded the command fails; gaps in a single route never fail it, they are printed as warnings. The child honours `WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS`.
16
+
17
+ ## Options
18
+
19
+ | Option | Default | Meaning |
20
+ | ----------------- | ---------------------------------------------- | ------------------------------------------------------------ |
21
+ | `--out`, `-o` | `storage/openapi/openapi.json` | Output file, relative to the project root. Parent folders are created. |
22
+ | `--title` | `name` in `package.json` (else `Warlock API`) | `info.title`. `info.version` is the `package.json` version (else `0.0.0`); `info.description` is its `description`. |
23
+ | `--server` | `http://<host>:<port>` from the `http` config | `servers[0].url`. A wildcard host (`0.0.0.0`, `::`) becomes `localhost`. |
24
+ | `--include-pages` | off | Also document page (SSR) routes; each is a `200` with `text/html`. |
25
+
26
+ ```bash
27
+ npx warlock generate.openapi --out docs/openapi.json --title "Shop API" --server https://api.shop.test
28
+ ```
29
+
30
+ ## What is documented
31
+
32
+ | Part | Source |
33
+ | ----------------------- | ----------------------------------------------------------------------------------------------- |
34
+ | Paths | `:id` becomes `{id}`. An `all` route expands into one operation per verb (get, post, put, patch, delete, options, head). |
35
+ | `operationId` | The route `name` (`name.<verb>` when an `all` route expands), else `<verb>_<path_slug>`. Duplicates are renamed with a warning. |
36
+ | Tag | The first static path segment (`/users/:id` is tagged `users`). |
37
+ | Summary / description | The route `label`; the route or handler `description`, plus a line about required user types and the cookie CSRF rule when the route is guarded. |
38
+ | Path parameters | `validation.params` gives typed ones; a path segment with no schema is a required `string`. |
39
+ | Request body / query | `validation.schema`, converted with the Seal schema's JSON Schema output. It is the JSON request body for POST/PUT/PATCH and query parameters for GET/HEAD/DELETE, following `validating` the way validation reads the request. |
40
+ | Responses | `handler.responseSchema`, one response per declared status (see below). A route with no 2xx declared gets a `200` description only. |
41
+ | `422` | Added when the route has `validation.schema`, shaped by `validation.response` (`errors`, `inputKey`, `inputError`, `status`; `Response.failedSchema` defaults to 422). Stored once as `components.schemas.ValidationFailed`. A route with only `validation.params` gets none. |
42
+ | `401` | Added when the route is guarded, as `{ error: string }` (`components.schemas.Unauthorized`). A status you declare yourself is kept. |
43
+ | Security | `authMiddleware()` tags the middleware it returns with a `Symbol.for("warlock.auth")` descriptor (`{ sources, userTypes }`); core reads it without importing auth. A header source is `bearerAuth` (HTTP bearer), a cookie source is `cookieAuth` (API key in that cookie); header plus cookie is an OR. Only the schemes a route uses are emitted. Middleware without a descriptor adds nothing. |
44
+
45
+ ### Responses from `responseSchema`
46
+
47
+ The grammar is the one described in [`create-controller`](../create-controller/SKILL.md#declaring-response-types-with-responseschema): cast strings with suffixes, a resource, `[Resource]`, nested objects.
48
+
49
+ ```ts
50
+ loginController.responseSchema = {
51
+ 200: { body: { user: UserResource, token: "string" } },
52
+ 400: { body: { error: "string" } },
53
+ };
54
+ ```
55
+
56
+ becomes
57
+
58
+ ```json
59
+ "200": {
60
+ "description": "Successful response",
61
+ "content": {
62
+ "application/json": {
63
+ "schema": {
64
+ "type": "object",
65
+ "properties": {
66
+ "user": { "$ref": "#/components/schemas/UserResource" },
67
+ "token": { "type": "string" }
68
+ },
69
+ "required": ["user", "token"]
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ - A cast maps to JSON Schema: `string` and `localized` to `string`, `url`/`uploadsUrl`/`storageUrl` to `string` with `format: uri`, `number`/`float` to `number`, `int` to `integer`, `boolean`, `object`, `array`, and `date` to the default date object (`iso`, `format`, `timestamp`, `humanTime`). `x[]` is an array; `x?` is `type: [T, "null"]`.
77
+ - A resource becomes a `components.schemas` entry named after its export (`UserResource`) and every use is a `$ref`. Nested, lazy and `"self"` fields reference the same entry. A resource that is not exported from a `*.resource.ts(x)` file gets a generic name.
78
+ - Every listed key is marked `required`.
79
+
80
+ ## Warnings
81
+
82
+ What cannot be described statically is documented as `{}` and listed after the run. Common ones: a field builder or resolver function in a resource, an unknown cast, a `validation.validate` hook (custom middleware is not documented), a `validation.schema` that is not an object schema, an already-documented method+path, and a malformed auth descriptor. Fix them by declaring the field with a cast string.
83
+
84
+ ## Live in development
85
+
86
+ With `@warlock.js/devtools` installed (`warlock add devtools`), `warlock dev` serves the same document:
87
+
88
+ - `http://localhost:<port>/__warlock/docs` renders it with Scalar. The dashboard has an **API** tab that embeds the same page.
89
+ - `GET /__warlock/api/openapi.json` returns the raw document, rebuilt from the routes registered in the running process on every request; it answers `503` when the host provides no generator.
90
+ - It is built in-process by `getDevelopmentOpenApiDocument()` (exported from `@warlock.js/core`; it throws outside `warlock dev`), so it follows your edits with no CLI run. Page routes are not included; each distinct warning is logged once per process.
91
+ - Devtools is dev-only and loopback-only, and the page makes no outside request (Scalar is vendored, no CDN).
92
+
93
+ ## Use the file in a client
94
+
95
+ The output is plain OpenAPI 3.1 JSON, so any tool that reads an OpenAPI file can use it: import `storage/openapi/openapi.json` into Postman or Insomnia (their Import action), or feed it to a client generator. Support for 3.1 is the tool's; if an old version refuses it, update the tool. Regenerate the file whenever routes change, or commit it if you publish it.
96
+
97
+ ## Postman
98
+
99
+ `warlock generate.postman` writes a Postman Collection v2.1.0. It is derived from the OpenAPI document (the same registration child and options as `generate.openapi`), never from the routes directly, so the collection and the OpenAPI file always agree.
100
+
101
+ ```bash
102
+ npx warlock generate.postman
103
+ # Postman collection written to <root>/storage/postman/collection.json (3 folders, 17 requests). Import it in Postman and set the {{token}} variable.
104
+ ```
105
+
106
+ | Option | Default | Meaning |
107
+ | ----------------- | ----------------------------------------- | ------------------------------------------------------------------ |
108
+ | `--out`, `-o` | `storage/postman/collection.json` | Output file, relative to the project root. |
109
+ | `--title` | `name` in `package.json` | Collection name; the collection id is a stable hash of it. |
110
+ | `--server` | `http://<host>:<port>` from the `http` config | Value of the `{{baseUrl}}` variable. |
111
+ | `--include-pages` | off | Also include page (SSR) routes. |
112
+
113
+ What the collection contains:
114
+
115
+ - One folder per tag (the first path segment), in order of first appearance; untagged operations sit at the root.
116
+ - One request per operation, named by its summary (the route `label`) or `operationId`. `{id}` becomes the `:id` path variable; query parameters are rows (optional ones disabled); the operation description and parameter descriptions carry over.
117
+ - A JSON body with a `Content-Type` header and an **example generated from the schema** (strings by format such as `email` or `date-time`, `0`, `false`, the first `enum` value, one array item, resources resolved with cycles cut). A multipart body becomes form-data with file rows.
118
+ - A saved response for every declared status, with an example body.
119
+ - Collection variables `baseUrl` and `token`. When any route is guarded by `authMiddleware()`, the collection uses bearer auth with `{{token}}`; public routes are set to no auth so the token is not sent. A cookie-only route has a description note: Postman keeps cookies per domain, so sign in against `{{baseUrl}}` first.
120
+
121
+ To use it: in Postman choose **Import** and pick the file, open the collection's **Variables** tab, paste a token into `token` (for example the `token` from your login response), and adjust `baseUrl` if needed. Regenerate and re-import when routes change.
122
+
123
+ Programmatic use: `openApiToPostmanCollection(document, { title?, baseUrl? })` is exported from `@warlock.js/core` (pure and deterministic), and `getDevelopmentPostmanCollection()` returns `{ collection, warnings }` for the running dev process (it throws outside `warlock dev`, like `getDevelopmentOpenApiDocument`).
124
+
125
+ ## Programmatic use
126
+
127
+ `buildOpenApiDocument(routes, context)` is exported from `@warlock.js/core`: a pure function from `router.list()` routes to `{ document, warnings }`. `context` carries `info`, `servers`, `includePages`, `resolveResourceName` and `validationResponse`. Use it to build the document inside your own tooling; the CLI uses the same function inside the registration child.
128
+
129
+ ```ts
130
+ import { buildOpenApiDocument, router } from "@warlock.js/core";
131
+
132
+ const { document, warnings } = buildOpenApiDocument(router.list(), {
133
+ info: { title: "Shop API", version: "1.0.0" },
134
+ });
135
+ ```
136
+
137
+ ## Gotchas
138
+
139
+ - **The document describes what you declare.** A route with no `responseSchema` documents only a `200` description; nothing is inferred from the controller body.
140
+ - **Export each resource from its own `*.resource.ts` file** so the schema component is named after it.
141
+ - **Seal validators are converted in the child process**, so the document always reflects the schema the app actually registers.
142
+ - **The 401 body is `{ error: string }`.** The auth middleware's real rejection also carries an `errorCode`; the document does not list it.
143
+
144
+ ## See also
145
+
146
+ - [`create-controller/SKILL.md`](../create-controller/SKILL.md) — `validation`, `description` and `responseSchema` on a handler.
147
+ - [`define-resource/SKILL.md`](../define-resource/SKILL.md) — resources, which become `components.schemas`.
148
+ - [`warlock-routes/SKILL.md`](../warlock-routes/SKILL.md) — list the routes the document is built from.
package/skills/index.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "The Warlock server framework: HTTP routes, controllers, responses, resources, repositories, CLI, mail, storage, security helpers. Exports `router`, `Request`, `Response`, `RequestHandler`, `useCase`, `RepositoryManager`, `defineResource`, `defineConfig`, `storage`, `sendMail`, `hashPassword`, `command`. Use for: add a route, write a controller, return a JSON response, hide a field from the response, upload a file, send an email, hash a password, add a CLI command, wire a socket, run or configure the app. Not this package: database models/queries → @warlock.js/cascade; input schemas → @warlock.js/seal; SSR pages → @warlock.js/web."
2
+ description: "The Warlock server framework: HTTP routes, controllers, responses, resources, repositories, CLI, mail, storage, security helpers. Exports `router`, `Request`, `Response`, `RequestHandler`, `useCase`, `RepositoryManager`, `defineResource`, `defineConfig`, `storage`, `sendMail`, `hashPassword`, `command`. Use for: add a route, write a controller, return a JSON response, hide a field from the response, upload a file, send an email, hash a password, add a CLI command, generate an OpenAPI document, type an API response, wire a socket, run or configure the app. Not this package: database models/queries → @warlock.js/cascade; input schemas → @warlock.js/seal; SSR pages → @warlock.js/web."
3
3
  ---
4
4
  # @warlock.js/core
5
5
 
@@ -9,7 +9,7 @@ Core is the application runtime: it boots the app, auto-loads each module's spec
9
9
  1. Scaffold a module (`create-module.md`), then declare routes in its `routes.ts` (`register-route.md`, `warlock-routes.md`).
10
10
  2. Write a thin controller (`create-controller.md`); validate input with a schema attached to the handler (`validate-input.md`).
11
11
  3. Put logic in a service or use case (`write-use-case.md`), and data access in a repository (`use-repository.md`).
12
- 4. Return via `response.*` (`send-response.md`) and shape output with a resource (`define-resource.md`).
12
+ 4. Return via `response.*` (`send-response.md`) and shape output with a resource (`define-resource.md`); declare `responseSchema` for typed clients and docs (`create-controller.md`, `generate-openapi.md`).
13
13
  5. Configure and run (`configure-app.md`, `run-app.md`); check setup with `warlock-doctor.md`.
14
14
 
15
15
  ## Topics by area
@@ -17,7 +17,7 @@ Core is the application runtime: it boots the app, auto-loads each module's spec
17
17
  - **HTTP & routing:** register-route, warlock-routes, create-controller, use-middleware, write-middleware, validate-input, use-request-locals, build-restful, wire-socket
18
18
  - **Responses & resources:** send-response, define-resource, use-model-transformers, use-localization
19
19
  - **Domain layers:** write-use-case, use-repository, write-seeder
20
- - **CLI & running the app:** run-app, write-cli-command, add-connector, health-checks, warlock-doctor
20
+ - **CLI & running the app:** run-app, write-cli-command, add-connector, health-checks, warlock-doctor, generate-openapi
21
21
  - **Storage, files & mail:** store-file, upload-file, process-image, send-mail
22
22
  - **Security & utilities:** hash-password, encrypt-data, retry-operation, benchmark-code, request-memo, request-tracing
23
23
  - **Testing:** test-http, test-service
@@ -185,10 +185,10 @@ const updateAvatarSchema = v.object({
185
185
 
186
186
  ## What the framework sends on failure
187
187
 
188
- The framework calls `response.failedSchema(result)` which sends `400` with the shape configured under `validation.response` (defaults shown):
188
+ The framework calls `response.failedSchema(result)` which sends `422` by default (set `validation.response.status` to change it) with the shape configured under `validation.response` (defaults shown):
189
189
 
190
190
  ```jsonc
191
- // 400 Bad Request
191
+ // 422 Unprocessable Entity
192
192
  {
193
193
  "errors": [
194
194
  { "input": "email", "error": "The email must be a valid email" },