@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
package/llms-full.txt CHANGED
@@ -1280,7 +1280,7 @@ Internal `@warlock.js/*` packages read from `config.get("<name>")` the same way
1280
1280
 
1281
1281
  ---
1282
1282
  name: create-controller
1283
- description: 'Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.'
1283
+ description: 'Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `controller.responseSchema`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type", "type the API response"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.'
1284
1284
  ---
1285
1285
 
1286
1286
  # Warlock — create a controller
@@ -1414,7 +1414,44 @@ Use `RequestHandler<Request<TSchema>>` for public routes, `GuardedRequestHandler
1414
1414
  createProductController.description = "Create a new product (admin only)";
1415
1415
  ```
1416
1416
 
1417
- Used by OpenAPI/Swagger generation (planned per `domains/core/backlog.md`) and surfaces in dev-server logs.
1417
+ `description` feeds the OpenAPI document (`warlock generate.openapi`, see [`generate-openapi`](../generate-openapi/SKILL.md)) and surfaces in dev-server logs.
1418
+
1419
+ ## Declaring response types with `responseSchema`
1420
+
1421
+ Set `responseSchema` on the handler, next to `validation`, to declare the body per status code. The same declaration types the client (web's `ApiRouteRegistry`, `useSubmitForm`) and fills the `responses` of the OpenAPI document.
1422
+
1423
+ ```ts title="src/app/auth/controllers/login.controller.ts"
1424
+ import type { RequestHandler } from "@warlock.js/core";
1425
+ import { loginUseCase } from "app/auth/use-cases/login.use-case";
1426
+ import { UserResource } from "app/users/resources/user.resource";
1427
+
1428
+ export const loginController: RequestHandler = async ({ request, response }) => {
1429
+ const { user, token } = await loginUseCase(request.all());
1430
+
1431
+ return response.success({ user, token });
1432
+ };
1433
+
1434
+ loginController.responseSchema = {
1435
+ 200: { body: { user: UserResource, token: "string" } },
1436
+ 400: { body: { error: "string" } },
1437
+ };
1438
+ ```
1439
+
1440
+ A body is an object whose values are one of:
1441
+
1442
+ | Value | Meaning |
1443
+ | ----------------------------------------------- | ------------------------------------------- |
1444
+ | a cast string, with the resource suffixes | `"string"`, `"number?"`, `"string[]"`, `"date[]?"` (`?` is `T \| null`, key always present) |
1445
+ | a resource | one `ResourceOutput` object |
1446
+ | `[Resource]` | an array of that resource |
1447
+ | a nested plain object of any of the above | recursed |
1448
+
1449
+ The output types follow the same rules as `defineResource` (`ResponseBodyOutput<B>` and `ResponseSchemaOutput<S>` are exported if you need the type of a body or of the whole schema).
1450
+
1451
+ - Types are generated, not annotated: `warlock generate.typings` and `warlock dev` load the routes, read each `responseSchema` and hand the response types of every named route to the web route-types generator. A route without `responseSchema` gets no `response` entry.
1452
+ - A resource is mapped back to its `*.resource.ts(x)` export by identity. A resource defined inline or outside such a file is typed `unknown`, and a warning names the route and field. Export each resource from its own `*.resource.ts` file.
1453
+ - `responseSchema` is kept when a route is bound from an array handler (`[controller, "action"]`) or a restful resource method.
1454
+ - Nothing checks the response against the schema at runtime: it is documentation and types.
1418
1455
 
1419
1456
  ## What belongs in a controller (and what doesn't)
1420
1457
 
@@ -1719,7 +1756,7 @@ await warmupProductCache();
1719
1756
 
1720
1757
  ---
1721
1758
  name: define-resource
1722
- 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.'
1759
+ 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.'
1723
1760
  ---
1724
1761
 
1725
1762
  # Warlock — define a resource
@@ -1915,6 +1952,8 @@ export const ChatResource = defineResource({
1915
1952
  transform(data, resource) {
1916
1953
  // final pass — mutate `data` in place; the return value is ignored
1917
1954
  data.slug = String(data.title).toLowerCase();
1955
+
1956
+ return data;
1918
1957
  },
1919
1958
  });
1920
1959
  ```
@@ -1923,6 +1962,103 @@ export const ChatResource = defineResource({
1923
1962
 
1924
1963
  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.
1925
1964
 
1965
+ ## Typed output
1966
+
1967
+ `defineResource()` reads the literal cast strings in `schema` and types `toJSON()` with them. Nothing is annotated by hand.
1968
+
1969
+ ```ts title="src/app/users/resources/user.resource.ts"
1970
+ import { defineResource, type ResourceOutput } from "@warlock.js/core";
1971
+
1972
+ export const UserResource = defineResource({
1973
+ schema: {
1974
+ id: "number",
1975
+ name: "string?",
1976
+ tags: "string[]",
1977
+ createdAt: "date",
1978
+ },
1979
+ });
1980
+
1981
+ export type UserJson = ResourceOutput<typeof UserResource>;
1982
+ // { id: number; name: string | null; tags: string[]; createdAt: { iso: string; format: string; timestamp: number; humanTime: string } }
1983
+
1984
+ const json = new UserResource(user).toJSON(); // typed as UserJson
1985
+ ```
1986
+
1987
+ `ResourceOutput<R>` takes the resource class or an instance (`InstanceType<typeof UserResource>`).
1988
+
1989
+ How each schema entry is typed:
1990
+
1991
+ | Schema entry | Output type |
1992
+ | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
1993
+ | `"string"`, `"localized"`, `"url"`, `"uploadsUrl"`, `"storageUrl"` | `string` |
1994
+ | `"number"`, `"float"`, `"int"` | `number` |
1995
+ | `"boolean"` | `boolean` |
1996
+ | `"date"` | `{ iso: string; format: string; timestamp: number; humanTime: string }` (the default date output) |
1997
+ | `"object"` / `"array"` | `Record<string, unknown>` / `unknown[]` |
1998
+ | `"x[]"` | array of the base type |
1999
+ | `"x?"` | `T \| null`; the key is always present, never optional |
2000
+ | a nested resource, `lazy(() => R)` | that resource's output (one object, even if the model holds an array) |
2001
+ | `"self"` / `"self[]"` | this resource's own output / an array of it |
2002
+ | `["inputKey", "cast"]` tuple | the cast's type |
2003
+ | a resolver function | its return type (an `any` return becomes `unknown`) |
2004
+ | a field builder (`this.date()...`, `new ResourceFieldBuilder(...)`) | `unknown` |
2005
+
2006
+ 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:
2007
+
2008
+ ```ts title="src/app/products/resources/product.resource.ts"
2009
+ import { defineResource } from "@warlock.js/core";
2010
+
2011
+ type ProductJson = { id: number; title: string; priceLabel: string };
2012
+
2013
+ export const ProductResource = defineResource<ProductJson>({
2014
+ schema: { id: "number", title: "string", price: "number" },
2015
+ transform: (data) => {
2016
+ data.priceLabel = `$${data.price}`;
2017
+ delete data.price;
2018
+
2019
+ return data;
2020
+ },
2021
+ });
2022
+ ```
2023
+
2024
+ `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.
2025
+
2026
+ ### `Serialized<T, W>` — what the client receives
2027
+
2028
+ `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`.
2029
+
2030
+ ```ts
2031
+ import type { Serialized } from "@warlock.js/core";
2032
+
2033
+ type Payload = Serialized<{ createdAt: Date; note?: string }>;
2034
+ // { createdAt: string; note?: string }
2035
+ ```
2036
+
2037
+ Rules, in the order the runtime applies them:
2038
+
2039
+ - Primitives, `Buffer` and `Uint8Array` pass through. `any` and `unknown` stay as they are.
2040
+ - On `"devalue"`, `Date`, `RegExp`, `URL`, `Map` and `Set` stay native. On `"json"`, a `Map` becomes a record and a `Set` an array.
2041
+ - A cascade model becomes its resource's output (next section); with no registry entry it becomes its serialized `data` type.
2042
+ - Anything with `toJSON()` becomes the (awaited) serialized result, which is how a `Date` becomes a `string` on `"json"`.
2043
+ - Arrays map item by item. Functions are dropped from objects.
2044
+ - On `"json"`, a key whose value can be `undefined` becomes optional, because `JSON.stringify` omits it.
2045
+ - 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"`.
2046
+ - Nesting is walked to a fixed depth (8), then the type is `unknown`.
2047
+
2048
+ ### `ModelResourceRegistry` — map a model to its resource
2049
+
2050
+ `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`:
2051
+
2052
+ ```ts title=".warlock/typings/model-resources.d.ts (generated)"
2053
+ declare module "@warlock.js/core" {
2054
+ interface ModelResourceRegistry {
2055
+ "User": { model: import("../../src/app/users/models/user.model").User; resource: typeof import("../../src/app/users/resources/user.resource").UserResource };
2056
+ }
2057
+ }
2058
+ ```
2059
+
2060
+ `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.
2061
+
1926
2062
  ## Attaching to a model
1927
2063
 
1928
2064
  Models reference their default resource via a static `resource` field. Repositories and other helpers can then serialize without importing the resource:
@@ -2232,6 +2368,158 @@ Password hashing (`hashPassword`) needs `bcryptjs`, which is its own install —
2232
2368
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — service layering, where the encryption boundary should sit.
2233
2369
 
2234
2370
 
2371
+ ## generate-openapi `@warlock.js/core/generate-openapi/SKILL.md`
2372
+
2373
+ ---
2374
+ name: generate-openapi
2375
+ 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`.'
2376
+ ---
2377
+
2378
+ # Warlock — generate an OpenAPI document
2379
+
2380
+ `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.
2381
+
2382
+ ```bash
2383
+ npx warlock generate.openapi
2384
+ # OpenAPI 3.1.0 written to <root>/storage/openapi/openapi.json (12 paths, 17 operations).
2385
+ ```
2386
+
2387
+ 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`.
2388
+
2389
+ ## Options
2390
+
2391
+ | Option | Default | Meaning |
2392
+ | ----------------- | ---------------------------------------------- | ------------------------------------------------------------ |
2393
+ | `--out`, `-o` | `storage/openapi/openapi.json` | Output file, relative to the project root. Parent folders are created. |
2394
+ | `--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`. |
2395
+ | `--server` | `http://<host>:<port>` from the `http` config | `servers[0].url`. A wildcard host (`0.0.0.0`, `::`) becomes `localhost`. |
2396
+ | `--include-pages` | off | Also document page (SSR) routes; each is a `200` with `text/html`. |
2397
+
2398
+ ```bash
2399
+ npx warlock generate.openapi --out docs/openapi.json --title "Shop API" --server https://api.shop.test
2400
+ ```
2401
+
2402
+ ## What is documented
2403
+
2404
+ | Part | Source |
2405
+ | ----------------------- | ----------------------------------------------------------------------------------------------- |
2406
+ | Paths | `:id` becomes `{id}`. An `all` route expands into one operation per verb (get, post, put, patch, delete, options, head). |
2407
+ | `operationId` | The route `name` (`name.<verb>` when an `all` route expands), else `<verb>_<path_slug>`. Duplicates are renamed with a warning. |
2408
+ | Tag | The first static path segment (`/users/:id` is tagged `users`). |
2409
+ | 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. |
2410
+ | Path parameters | `validation.params` gives typed ones; a path segment with no schema is a required `string`. |
2411
+ | 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. |
2412
+ | Responses | `handler.responseSchema`, one response per declared status (see below). A route with no 2xx declared gets a `200` description only. |
2413
+ | `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. |
2414
+ | `401` | Added when the route is guarded, as `{ error: string }` (`components.schemas.Unauthorized`). A status you declare yourself is kept. |
2415
+ | 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. |
2416
+
2417
+ ### Responses from `responseSchema`
2418
+
2419
+ 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.
2420
+
2421
+ ```ts
2422
+ loginController.responseSchema = {
2423
+ 200: { body: { user: UserResource, token: "string" } },
2424
+ 400: { body: { error: "string" } },
2425
+ };
2426
+ ```
2427
+
2428
+ becomes
2429
+
2430
+ ```json
2431
+ "200": {
2432
+ "description": "Successful response",
2433
+ "content": {
2434
+ "application/json": {
2435
+ "schema": {
2436
+ "type": "object",
2437
+ "properties": {
2438
+ "user": { "$ref": "#/components/schemas/UserResource" },
2439
+ "token": { "type": "string" }
2440
+ },
2441
+ "required": ["user", "token"]
2442
+ }
2443
+ }
2444
+ }
2445
+ }
2446
+ ```
2447
+
2448
+ - 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"]`.
2449
+ - 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.
2450
+ - Every listed key is marked `required`.
2451
+
2452
+ ## Warnings
2453
+
2454
+ 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.
2455
+
2456
+ ## Live in development
2457
+
2458
+ With `@warlock.js/devtools` installed (`warlock add devtools`), `warlock dev` serves the same document:
2459
+
2460
+ - `http://localhost:<port>/__warlock/docs` renders it with Scalar. The dashboard has an **API** tab that embeds the same page.
2461
+ - `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.
2462
+ - 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.
2463
+ - Devtools is dev-only and loopback-only, and the page makes no outside request (Scalar is vendored, no CDN).
2464
+
2465
+ ## Use the file in a client
2466
+
2467
+ 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.
2468
+
2469
+ ## Postman
2470
+
2471
+ `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.
2472
+
2473
+ ```bash
2474
+ npx warlock generate.postman
2475
+ # Postman collection written to <root>/storage/postman/collection.json (3 folders, 17 requests). Import it in Postman and set the {{token}} variable.
2476
+ ```
2477
+
2478
+ | Option | Default | Meaning |
2479
+ | ----------------- | ----------------------------------------- | ------------------------------------------------------------------ |
2480
+ | `--out`, `-o` | `storage/postman/collection.json` | Output file, relative to the project root. |
2481
+ | `--title` | `name` in `package.json` | Collection name; the collection id is a stable hash of it. |
2482
+ | `--server` | `http://<host>:<port>` from the `http` config | Value of the `{{baseUrl}}` variable. |
2483
+ | `--include-pages` | off | Also include page (SSR) routes. |
2484
+
2485
+ What the collection contains:
2486
+
2487
+ - One folder per tag (the first path segment), in order of first appearance; untagged operations sit at the root.
2488
+ - 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.
2489
+ - 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.
2490
+ - A saved response for every declared status, with an example body.
2491
+ - 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.
2492
+
2493
+ 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.
2494
+
2495
+ 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`).
2496
+
2497
+ ## Programmatic use
2498
+
2499
+ `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.
2500
+
2501
+ ```ts
2502
+ import { buildOpenApiDocument, router } from "@warlock.js/core";
2503
+
2504
+ const { document, warnings } = buildOpenApiDocument(router.list(), {
2505
+ info: { title: "Shop API", version: "1.0.0" },
2506
+ });
2507
+ ```
2508
+
2509
+ ## Gotchas
2510
+
2511
+ - **The document describes what you declare.** A route with no `responseSchema` documents only a `200` description; nothing is inferred from the controller body.
2512
+ - **Export each resource from its own `*.resource.ts` file** so the schema component is named after it.
2513
+ - **Seal validators are converted in the child process**, so the document always reflects the schema the app actually registers.
2514
+ - **The 401 body is `{ error: string }`.** The auth middleware's real rejection also carries an `errorCode`; the document does not list it.
2515
+
2516
+ ## See also
2517
+
2518
+ - [`create-controller/SKILL.md`](../create-controller/SKILL.md) — `validation`, `description` and `responseSchema` on a handler.
2519
+ - [`define-resource/SKILL.md`](../define-resource/SKILL.md) — resources, which become `components.schemas`.
2520
+ - [`warlock-routes/SKILL.md`](../warlock-routes/SKILL.md) — list the routes the document is built from.
2521
+
2522
+
2235
2523
  ## hash-password `@warlock.js/core/hash-password/SKILL.md`
2236
2524
 
2237
2525
  ---
@@ -8428,10 +8716,10 @@ const updateAvatarSchema = v.object({
8428
8716
 
8429
8717
  ## What the framework sends on failure
8430
8718
 
8431
- The framework calls `response.failedSchema(result)` which sends `400` with the shape configured under `validation.response` (defaults shown):
8719
+ 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):
8432
8720
 
8433
8721
  ```jsonc
8434
- // 400 Bad Request
8722
+ // 422 Unprocessable Entity
8435
8723
  {
8436
8724
  "errors": [
8437
8725
  { "input": "email", "error": "The email must be a valid email" },
package/llms.txt CHANGED
@@ -11,10 +11,11 @@
11
11
  - [build-restful](@warlock.js/core/build-restful/SKILL.md): Generate standard CRUD endpoints — via `router.route(path).list().show().create().update().destroy()` chain or the `Restful` base class. Pick the chain by default; reach for `Restful` when you want repository-bound defaults. Triggers: `router.route`, `Restful`, `router.restfulResource`, `RouteResource`, `.crud`, `.nest`, `beforeCreate`, `onCreate`; "build a CRUD API", "register list/show/create/update/destroy", "repository-bound default handlers", "override a single REST action"; typical import `import { router, Restful } from "@warlock.js/core"`. Skip: wider router surface — `@warlock.js/core/register-route/SKILL.md`; per-action controllers — `@warlock.js/core/create-controller/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing pattern: hand-rolled controllers, `@nestjs/swagger` decorator-driven CRUD.
12
12
  - [build-url](@warlock.js/core/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — `@warlock.js/core/resolve-path/SKILL.md`; signed CDN URLs — `@warlock.js/core/store-file/SKILL.md`; resource output — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: hand-rolled `${baseUrl}/...` template strings.
13
13
  - [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
14
- - [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
14
+ - [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `controller.responseSchema`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type", "type the API response"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
15
15
  - [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
16
- - [define-resource](@warlock.js/core/define-resource/SKILL.md): 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.
16
+ - [define-resource](@warlock.js/core/define-resource/SKILL.md): 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.
17
17
  - [encrypt-data](@warlock.js/core/encrypt-data/SKILL.md): Reversible AES-256-GCM `encrypt` / `decrypt` for secrets you need to read back; one-way HMAC-SHA256 `hmacHash` for deterministic fingerprints (lookup/dedup of encrypted columns). Keys come from `src/config/encryption.ts`. Triggers: `encrypt`, `decrypt`, `hmacHash`, `EncryptionConfigurations`, `APP_ENCRYPTION_KEY`, `APP_HMAC_KEY`; "store an API key reversibly", "fingerprint an encrypted column for lookup", "AES-256-GCM secret", "HMAC-SHA256 dedup key"; typical import `import { encrypt, decrypt, hmacHash } from "@warlock.js/core"`. Skip: password hashing — `@warlock.js/core/hash-password/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs Node `crypto` direct, `crypto-js`, `libsodium-wrappers`.
18
+ - [generate-openapi](@warlock.js/core/generate-openapi/SKILL.md): 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`.
18
19
  - [hash-password](@warlock.js/core/hash-password/SKILL.md): One-way bcrypt password hashing — `hashPassword` / `verifyPassword`, plus the declarative `useHashedPassword()` schema transformer that auto-hashes a model's password field on save. Salt rounds come from `src/config/encryption.ts`. Triggers: `hashPassword`, `verifyPassword`, `useHashedPassword`, `password.salt`, `bcryptjs`; "hash a user password", "verify login credentials", "auto-hash on save", "rotate a password"; typical import `import { hashPassword, verifyPassword } from "@warlock.js/core"`. Skip: reversible secrets — `@warlock.js/core/encrypt-data/SKILL.md`; the other transformers — `@warlock.js/core/use-model-transformers/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `bcrypt` native, `argon2`, `scrypt`.
19
20
  - [health-checks](@warlock.js/core/health-checks/SKILL.md): Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — `@warlock.js/core/use-app-context/SKILL.md`; maintenance-mode 503s — `@warlock.js/core/use-middleware/SKILL.md`; connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers.
20
21
  - [lower-stage3-decorators](@warlock.js/core/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.
package/package.json CHANGED
@@ -23,14 +23,14 @@
23
23
  "@mongez/localization": "^3.5.1",
24
24
  "@mongez/reinforcements": "^4.0.2",
25
25
  "@mongez/slug": "^1.0.7",
26
- "@mongez/supportive-is": "^2.1.5",
26
+ "@mongez/supportive-is": "^3.0.0",
27
27
  "@mongez/time-wizard": "^1.0.6",
28
- "@warlock.js/cache": "5.26.0",
29
- "@warlock.js/cascade": "5.26.0",
30
- "@warlock.js/context": "5.26.0",
31
- "@warlock.js/fs": "5.26.0",
32
- "@warlock.js/logger": "5.26.0",
33
- "@warlock.js/seal": "5.26.0",
28
+ "@warlock.js/cache": "5.28.0",
29
+ "@warlock.js/cascade": "5.28.0",
30
+ "@warlock.js/context": "5.28.0",
31
+ "@warlock.js/fs": "5.28.0",
32
+ "@warlock.js/logger": "5.28.0",
33
+ "@warlock.js/seal": "5.28.0",
34
34
  "bcryptjs": "^3.0.3",
35
35
  "chokidar": "^5.0.0",
36
36
  "dayjs": "^1.11.19",
@@ -52,11 +52,11 @@
52
52
  "@aws-sdk/lib-storage": "^3.955.0",
53
53
  "@aws-sdk/s3-request-presigner": "^3.955.0",
54
54
  "@react-email/render": "^2.0.5",
55
- "@warlock.js/access": "5.26.0",
56
- "@warlock.js/ai": "5.26.0",
57
- "@warlock.js/herald": "5.26.0",
58
- "@warlock.js/notifications": "5.26.0",
59
- "@warlock.js/devtools": "5.26.0",
55
+ "@warlock.js/access": "5.28.0",
56
+ "@warlock.js/ai": "5.28.0",
57
+ "@warlock.js/herald": "5.28.0",
58
+ "@warlock.js/notifications": "5.28.0",
59
+ "@warlock.js/devtools": "5.28.0",
60
60
  "nodemailer": "^8.0.5",
61
61
  "react": "^19.2.3",
62
62
  "react-dom": "^19.2.3",
@@ -127,7 +127,7 @@
127
127
  ],
128
128
  "author": "hassanzohdy",
129
129
  "license": "MIT",
130
- "version": "5.26.0",
130
+ "version": "5.28.0",
131
131
  "type": "module",
132
132
  "main": "./esm/index.mjs",
133
133
  "module": "./esm/index.mjs",
package/skills/README.md CHANGED
@@ -6,7 +6,7 @@ Each folder holds one `SKILL.md` (an agent-facing how-to for a single task). The
6
6
 
7
7
  - [register-route](register-route/SKILL.md) — register single routes, prefix groups, middleware-guarded blocks, and RESTful resource chains.
8
8
  - [build-restful](build-restful/SKILL.md) — generate standard CRUD endpoints via the `router.route(...)` chain or the `Restful` base class.
9
- - [create-controller](create-controller/SKILL.md) — author HTTP controllers: `RequestHandler` signature, validated input, response helpers, middleware.
9
+ - [create-controller](create-controller/SKILL.md) — author HTTP controllers: `RequestHandler` signature, validated input, response helpers, middleware, `responseSchema` response types.
10
10
  - [send-response](send-response/SKILL.md) — `Response` helpers: success/error variants, status helpers, redirects, files, streams, SSE.
11
11
  - [validate-input](validate-input/SKILL.md) — author seal schemas, attach them to controllers, infer types, layer DTOs.
12
12
  - [use-middleware](use-middleware/SKILL.md) — attach built-in HTTP middleware (rateLimit, concurrencyLimit, maxBodySize, …) via the `middleware` namespace.
@@ -19,7 +19,7 @@ Each folder holds one `SKILL.md` (an agent-facing how-to for a single task). The
19
19
 
20
20
  - [use-repository](use-repository/SKILL.md) — subclass `RepositoryManager`: `source`/`filterBy`/`defaultOptions`, list/find/CRUD, cached/cursor variants, and the `filterBy`-aware aggregates (`sum`/`avg`/`min`/`max`/`groupBy`/`aggregate`).
21
21
  - [use-model-transformers](use-model-transformers/SKILL.md) — schema-side helpers: `useHashedPassword()`, `useComputedSlug()`, and friends.
22
- - [define-resource](define-resource/SKILL.md) — map model fields to wire-shape via `defineResource()` / `Resource` subclasses (output-only).
22
+ - [define-resource](define-resource/SKILL.md) — map model fields to wire-shape via `defineResource()` / `Resource` subclasses (output-only); typed output with `ResourceOutput`, `Serialized<T, W>` and `ModelResourceRegistry`.
23
23
  - [write-seeder](write-seeder/SKILL.md) — author a seed file with `seeder()` — `name`/`dependsOn`/`once`/`order`/`batchSize`, `run({ track, now, batchSize })`, `warlock seed --drop`.
24
24
 
25
25
  ## Files, media, mail
@@ -46,6 +46,7 @@ Each folder holds one `SKILL.md` (an agent-facing how-to for a single task). The
46
46
  - [warlock-doctor](warlock-doctor/SKILL.md) — `warlock doctor`: read-only diagnostics (routes/config/connectors/optional-peers/health/release-hygiene) with a pass/warn/fail report and non-zero exit on failure.
47
47
  - [warlock-routes](warlock-routes/SKILL.md) — `warlock routes`: list the registered HTTP routes as a verb-colored table (method/path/name/action/middleware/source); filter by method/path/name or emit JSON. Read-only, no connectors.
48
48
  - [write-cli-command](write-cli-command/SKILL.md) — author a custom `warlock <cmd>` via the `command()` factory, or inspect built-in `warlock add` feature scaffolding such as the Web starter.
49
+ - [generate-openapi](generate-openapi/SKILL.md) — `warlock generate.openapi`: an OpenAPI 3.1 document from the routes (validation, `responseSchema`, auth), plus the dev-only API docs page.
49
50
  - [run-app](run-app/SKILL.md) — `warlock dev` / `warlock build` / `warlock start` operational commands.
50
51
  - [update-packages](update-packages/SKILL.md) — bump every `@warlock.js/*` dependency with `warlock update`.
51
52
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: create-controller
3
- description: 'Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.'
3
+ description: 'Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `controller.responseSchema`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type", "type the API response"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.'
4
4
  ---
5
5
 
6
6
  # Warlock — create a controller
@@ -134,7 +134,44 @@ Use `RequestHandler<Request<TSchema>>` for public routes, `GuardedRequestHandler
134
134
  createProductController.description = "Create a new product (admin only)";
135
135
  ```
136
136
 
137
- Used by OpenAPI/Swagger generation (planned per `domains/core/backlog.md`) and surfaces in dev-server logs.
137
+ `description` feeds the OpenAPI document (`warlock generate.openapi`, see [`generate-openapi`](../generate-openapi/SKILL.md)) and surfaces in dev-server logs.
138
+
139
+ ## Declaring response types with `responseSchema`
140
+
141
+ Set `responseSchema` on the handler, next to `validation`, to declare the body per status code. The same declaration types the client (web's `ApiRouteRegistry`, `useSubmitForm`) and fills the `responses` of the OpenAPI document.
142
+
143
+ ```ts title="src/app/auth/controllers/login.controller.ts"
144
+ import type { RequestHandler } from "@warlock.js/core";
145
+ import { loginUseCase } from "app/auth/use-cases/login.use-case";
146
+ import { UserResource } from "app/users/resources/user.resource";
147
+
148
+ export const loginController: RequestHandler = async ({ request, response }) => {
149
+ const { user, token } = await loginUseCase(request.all());
150
+
151
+ return response.success({ user, token });
152
+ };
153
+
154
+ loginController.responseSchema = {
155
+ 200: { body: { user: UserResource, token: "string" } },
156
+ 400: { body: { error: "string" } },
157
+ };
158
+ ```
159
+
160
+ A body is an object whose values are one of:
161
+
162
+ | Value | Meaning |
163
+ | ----------------------------------------------- | ------------------------------------------- |
164
+ | a cast string, with the resource suffixes | `"string"`, `"number?"`, `"string[]"`, `"date[]?"` (`?` is `T \| null`, key always present) |
165
+ | a resource | one `ResourceOutput` object |
166
+ | `[Resource]` | an array of that resource |
167
+ | a nested plain object of any of the above | recursed |
168
+
169
+ The output types follow the same rules as `defineResource` (`ResponseBodyOutput<B>` and `ResponseSchemaOutput<S>` are exported if you need the type of a body or of the whole schema).
170
+
171
+ - Types are generated, not annotated: `warlock generate.typings` and `warlock dev` load the routes, read each `responseSchema` and hand the response types of every named route to the web route-types generator. A route without `responseSchema` gets no `response` entry.
172
+ - A resource is mapped back to its `*.resource.ts(x)` export by identity. A resource defined inline or outside such a file is typed `unknown`, and a warning names the route and field. Export each resource from its own `*.resource.ts` file.
173
+ - `responseSchema` is kept when a route is bound from an array handler (`[controller, "action"]`) or a restful resource method.
174
+ - Nothing checks the response against the schema at runtime: it is documentation and types.
138
175
 
139
176
  ## What belongs in a controller (and what doesn't)
140
177