@warlock.js/core 5.25.0 → 5.27.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.
- package/CHANGELOG.md +53 -0
- package/esm/cli/commands/doctor/checks/index.mjs +2 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +5 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/openapi-generator.action.mjs +43 -0
- package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
- package/esm/cli/commands/openapi-generator.command.mjs +44 -0
- package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
- package/esm/cli/commands/typings-generator.command.mjs +4 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +2 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/build-development-openapi-document.mjs +36 -0
- package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
- package/esm/dev-server/collect-development-api-routes.mjs +27 -0
- package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
- package/esm/dev-server/dev-logger.mjs +9 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +6 -3
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +17 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/legacy-locales-warning.mjs +16 -0
- package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
- package/esm/dev-server/model-resource-typings.mjs +246 -0
- package/esm/dev-server/model-resource-typings.mjs.map +1 -0
- package/esm/dev-server/module-loader.mjs +16 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +2 -2
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +7 -3
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +73 -6
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +38 -18
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/bull-board.feature.mjs +2 -2
- package/esm/generations/features/bull-board.feature.mjs.map +1 -1
- package/esm/http/build-rate-limit-options.d.mts +8 -2
- package/esm/http/build-rate-limit-options.d.mts.map +1 -1
- package/esm/http/build-rate-limit-options.mjs +1 -0
- package/esm/http/build-rate-limit-options.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +1 -17
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +1 -20
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +4 -3
- package/esm/http/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/request.d.mts +9 -26
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +1 -22
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +4 -2
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/serialized.d.mts +114 -0
- package/esm/http/serialized.d.mts.map +1 -0
- package/esm/index.d.mts +12 -5
- package/esm/index.mjs +8 -2
- package/esm/localization/index.mjs +4 -0
- package/esm/localization/parse-locale-dictionary.d.mts +23 -0
- package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/parse-locale-dictionary.mjs +215 -0
- package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
- package/esm/localization/register-locale-dictionary.d.mts +12 -0
- package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/register-locale-dictionary.mjs +58 -0
- package/esm/localization/register-locale-dictionary.mjs.map +1 -0
- package/esm/openapi/build-openapi-document.d.mts +17 -0
- package/esm/openapi/build-openapi-document.d.mts.map +1 -0
- package/esm/openapi/build-openapi-document.mjs +124 -0
- package/esm/openapi/build-openapi-document.mjs.map +1 -0
- package/esm/openapi/cast-schema.mjs +89 -0
- package/esm/openapi/cast-schema.mjs.map +1 -0
- package/esm/openapi/collect-openapi-document.mjs +40 -0
- package/esm/openapi/collect-openapi-document.mjs.map +1 -0
- package/esm/openapi/development-openapi-provider.d.mts +17 -0
- package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
- package/esm/openapi/development-openapi-provider.mjs +21 -0
- package/esm/openapi/development-openapi-provider.mjs.map +1 -0
- package/esm/openapi/error-responses.mjs +67 -0
- package/esm/openapi/error-responses.mjs.map +1 -0
- package/esm/openapi/index.d.mts +3 -0
- package/esm/openapi/index.mjs +4 -0
- package/esm/openapi/openapi-child-output.mjs +57 -0
- package/esm/openapi/openapi-child-output.mjs.map +1 -0
- package/esm/openapi/openapi-protocol.mjs +30 -0
- package/esm/openapi/openapi-protocol.mjs.map +1 -0
- package/esm/openapi/paths.mjs +63 -0
- package/esm/openapi/paths.mjs.map +1 -0
- package/esm/openapi/request-schemas.mjs +138 -0
- package/esm/openapi/request-schemas.mjs.map +1 -0
- package/esm/openapi/response-schemas.mjs +85 -0
- package/esm/openapi/response-schemas.mjs.map +1 -0
- package/esm/openapi/schema-registry.mjs +143 -0
- package/esm/openapi/schema-registry.mjs.map +1 -0
- package/esm/openapi/security.mjs +98 -0
- package/esm/openapi/security.mjs.map +1 -0
- package/esm/openapi/types.d.mts +119 -0
- package/esm/openapi/types.d.mts.map +1 -0
- package/esm/production/production-builder.mjs +11 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/registration-child-runner.mjs +139 -0
- package/esm/production/registration-child-runner.mjs.map +1 -0
- package/esm/production/route-registration-child.mjs +30 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +22 -119
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/define-resource.d.mts +44 -14
- package/esm/resource/define-resource.d.mts.map +1 -1
- package/esm/resource/define-resource.mjs +0 -40
- package/esm/resource/define-resource.mjs.map +1 -1
- package/esm/resource/index.d.mts +1 -1
- package/esm/resource/resource.d.mts +4 -4
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/resource/types.d.mts +89 -4
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/restful/restful.d.mts +5 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/router/index.d.mts +1 -1
- package/esm/router/named-api-routes-with-responses.mjs +59 -0
- package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
- package/esm/router/response-schema-types.mjs +100 -0
- package/esm/router/response-schema-types.mjs.map +1 -0
- package/esm/router/router.d.mts +2 -2
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +38 -11
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +24 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.d.mts +10 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +16 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +48 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +383 -80
- package/llms.txt +4 -3
- package/package.json +21 -21
- package/skills/README.md +4 -3
- package/skills/create-controller/SKILL.md +43 -2
- package/skills/define-resource/SKILL.md +100 -1
- package/skills/generate-openapi/SKILL.md +120 -0
- package/skills/index.md +3 -3
- package/skills/test-http/SKILL.md +9 -1
- package/skills/use-localization/SKILL.md +39 -26
- package/skills/validate-input/SKILL.md +2 -2
package/llms-full.txt
CHANGED
|
@@ -1197,6 +1197,8 @@ Same shape — TS validates the union; runtime picks the `default` key.
|
|
|
1197
1197
|
- `socket.adapter` — adapter factory for cross-server broadcast.
|
|
1198
1198
|
- `cache.silenceSingleServerWarning`, `storage.silenceSingleServerWarning`, `socket.silenceSingleServerWarning` — silence the production warnings for in-memory cache, local storage, and adapter-less sockets.
|
|
1199
1199
|
|
|
1200
|
+
`http.modelValidationErrorStatus` sets the status for a Cascade model-write validation failure (a model failing its own schema on save). It defaults to `500` with an opaque body, because request input is already validated with a `422` before the model is touched. Set a `4xx` (e.g. `422`) to treat it as a client error instead; the response then carries `{ errors }`.
|
|
1201
|
+
|
|
1200
1202
|
## Per-env knobs
|
|
1201
1203
|
|
|
1202
1204
|
For one-off switches gated on environment, the canonical pattern is `Application.isProduction` (also `isDevelopment`, `isTest`):
|
|
@@ -1278,7 +1280,7 @@ Internal `@warlock.js/*` packages read from `config.get("<name>")` the same way
|
|
|
1278
1280
|
|
|
1279
1281
|
---
|
|
1280
1282
|
name: create-controller
|
|
1281
|
-
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.'
|
|
1282
1284
|
---
|
|
1283
1285
|
|
|
1284
1286
|
# Warlock — create a controller
|
|
@@ -1380,6 +1382,10 @@ Two pieces:
|
|
|
1380
1382
|
|
|
1381
1383
|
If validation fails, the framework returns a 400 with an `errors` payload and your handler never runs.
|
|
1382
1384
|
|
|
1385
|
+
### Validating route params
|
|
1386
|
+
|
|
1387
|
+
`handler.validation = { params: v.object({ id: v.int().min(1) }) }` validates path segments (e.g. `/posts/:id`). Number and boolean validators convert the string segments; the app's schema object is never changed. Parsed values replace `request.params` (`request.input("id")` is a number) and merge into `request.validated()`, with params winning over body fields. Failures answer like a `schema` failure.
|
|
1388
|
+
|
|
1383
1389
|
### Typing a guarded handler
|
|
1384
1390
|
|
|
1385
1391
|
Routes behind `authMiddleware` need `request.locals.user` typed. Project conventions add a `GuardedRequest<TSchema>` (overriding `locals` to `RequestLocals & { user: User }`) and a paired `GuardedRequestHandler<TSchema>` alias in `app/auth/requests/guarded.request`:
|
|
@@ -1408,7 +1414,44 @@ Use `RequestHandler<Request<TSchema>>` for public routes, `GuardedRequestHandler
|
|
|
1408
1414
|
createProductController.description = "Create a new product (admin only)";
|
|
1409
1415
|
```
|
|
1410
1416
|
|
|
1411
|
-
|
|
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.
|
|
1412
1455
|
|
|
1413
1456
|
## What belongs in a controller (and what doesn't)
|
|
1414
1457
|
|
|
@@ -1713,7 +1756,7 @@ await warmupProductCache();
|
|
|
1713
1756
|
|
|
1714
1757
|
---
|
|
1715
1758
|
name: define-resource
|
|
1716
|
-
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.'
|
|
1717
1760
|
---
|
|
1718
1761
|
|
|
1719
1762
|
# Warlock — define a resource
|
|
@@ -1909,6 +1952,8 @@ export const ChatResource = defineResource({
|
|
|
1909
1952
|
transform(data, resource) {
|
|
1910
1953
|
// final pass — mutate `data` in place; the return value is ignored
|
|
1911
1954
|
data.slug = String(data.title).toLowerCase();
|
|
1955
|
+
|
|
1956
|
+
return data;
|
|
1912
1957
|
},
|
|
1913
1958
|
});
|
|
1914
1959
|
```
|
|
@@ -1917,6 +1962,103 @@ export const ChatResource = defineResource({
|
|
|
1917
1962
|
|
|
1918
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.
|
|
1919
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
|
+
|
|
1920
2062
|
## Attaching to a model
|
|
1921
2063
|
|
|
1922
2064
|
Models reference their default resource via a static `resource` field. Repositories and other helpers can then serialize without importing the resource:
|
|
@@ -1934,6 +2076,10 @@ export class Product extends Model {
|
|
|
1934
2076
|
|
|
1935
2077
|
This is convention, not a framework requirement. Controllers/services that import the resource explicitly work the same way.
|
|
1936
2078
|
|
|
2079
|
+
### `id` and Mongo `_id`
|
|
2080
|
+
|
|
2081
|
+
The output `id` falls back to `_id` when the record has no `id`; an explicit `id` always wins. If a collection uses native ObjectIds, set `public primaryKey = "_id"` on the model so `Model.find(hex)` resolves the `id` your resource emits.
|
|
2082
|
+
|
|
1937
2083
|
## Output-only rule
|
|
1938
2084
|
|
|
1939
2085
|
Resources map fields. They do not:
|
|
@@ -2222,6 +2368,130 @@ Password hashing (`hashPassword`) needs `bcryptjs`, which is its own install —
|
|
|
2222
2368
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — service layering, where the encryption boundary should sit.
|
|
2223
2369
|
|
|
2224
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`) 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`, `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
|
+
## Programmatic use
|
|
2470
|
+
|
|
2471
|
+
`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.
|
|
2472
|
+
|
|
2473
|
+
```ts
|
|
2474
|
+
import { buildOpenApiDocument, router } from "@warlock.js/core";
|
|
2475
|
+
|
|
2476
|
+
const { document, warnings } = buildOpenApiDocument(router.list(), {
|
|
2477
|
+
info: { title: "Shop API", version: "1.0.0" },
|
|
2478
|
+
});
|
|
2479
|
+
```
|
|
2480
|
+
|
|
2481
|
+
## Gotchas
|
|
2482
|
+
|
|
2483
|
+
- **The document describes what you declare.** A route with no `responseSchema` documents only a `200` description; nothing is inferred from the controller body.
|
|
2484
|
+
- **Export each resource from its own `*.resource.ts` file** so the schema component is named after it.
|
|
2485
|
+
- **Seal validators are converted in the child process**, so the document always reflects the schema the app actually registers.
|
|
2486
|
+
- **The 401 body is `{ error: string }`.** The auth middleware's real rejection also carries an `errorCode`; the document does not list it.
|
|
2487
|
+
|
|
2488
|
+
## See also
|
|
2489
|
+
|
|
2490
|
+
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — `validation`, `description` and `responseSchema` on a handler.
|
|
2491
|
+
- [`define-resource/SKILL.md`](../define-resource/SKILL.md) — resources, which become `components.schemas`.
|
|
2492
|
+
- [`warlock-routes/SKILL.md`](../warlock-routes/SKILL.md) — list the routes the document is built from.
|
|
2493
|
+
|
|
2494
|
+
|
|
2225
2495
|
## hash-password `@warlock.js/core/hash-password/SKILL.md`
|
|
2226
2496
|
|
|
2227
2497
|
---
|
|
@@ -3786,6 +4056,7 @@ export default defineConfig({
|
|
|
3786
4056
|
sourcemap: true, // default — true | false | "inline" | "linked"
|
|
3787
4057
|
singleBundle: false, // default — one runnable file, deps inlined
|
|
3788
4058
|
esmShim: true, // default — require/__filename/__dirname for bundled CJS
|
|
4059
|
+
routeRegistrationTimeoutMs: 30_000, // default — route-registration child deadline
|
|
3789
4060
|
},
|
|
3790
4061
|
});
|
|
3791
4062
|
```
|
|
@@ -3799,6 +4070,7 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
|
|
|
3799
4070
|
- **`sourcemap: false`** — skip source maps entirely. Smaller artifact, but stack traces in production logs lose their file:line precision (and `warlock start` will not enable `--enable-source-maps` since there's nothing to map).
|
|
3800
4071
|
- **`singleBundle: true`** — inline dependencies so `node dist/app.js` runs with no `node_modules` and no launcher. Sets `packages: "bundle"` + `splitting: false` as **defaults you can override**. ⚠ Native `.node` addons are still emitted alongside — "single bundle" is one JS file _plus_ any native addons. Do NOT reach for it as the default: keeping deps external is right when you deploy the folder.
|
|
3801
4072
|
- **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
|
|
4073
|
+
- **`routeRegistrationTimeoutMs`** — increase on busy CI runners when static route registration needs more than 30 seconds. `WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS` overrides this value for a build; invalid values warn and use 30,000ms.
|
|
3802
4074
|
|
|
3803
4075
|
⚠ **The trap this replaced.** Setting `packages: "bundle"` by hand used to produce a clean build whose process died on start with `Error: Dynamic require of "node:assert" is not supported`. The output is ESM; bundled CJS deps call `require(...)` and read `__dirname`, and neither exists in an ES module, so the bundler substitutes a throwing stub. **A green `warlock build` was not evidence the bundle ran.** `esmShim` now injects `createRequire(import.meta.url)` and friends automatically for any ESM build, so both `singleBundle` and a hand-written `packages: "bundle"` work. An existing hand-written `banner` is preserved — the shim is prepended, not substituted.
|
|
3804
4076
|
|
|
@@ -5265,7 +5537,7 @@ Some tests need the full stack: route matching, middleware chain, validation, co
|
|
|
5265
5537
|
|
|
5266
5538
|
`startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
|
|
5267
5539
|
|
|
5268
|
-
⚠ **Changed in 4.13.0 — the import is a subpath now.** These helpers used to be re-exported from the package root; they are not any more, because that put the test helpers into every application's production module graph. `import { testGet } from "@warlock.js/core"` now fails with
|
|
5540
|
+
⚠ **Changed in 4.13.0 — the import is a subpath now.** These helpers used to be re-exported from the package root; they are not any more, because that put the test helpers into every application's production module graph. `import { testGet } from "@warlock.js/core"` now fails with _"has no exported member"_ — **add `/tests` to the specifier and nothing else changes.**
|
|
5269
5541
|
|
|
5270
5542
|
## The shape
|
|
5271
5543
|
|
|
@@ -5306,7 +5578,7 @@ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests"
|
|
|
5306
5578
|
- Loads `warlock.config.ts` + bootstrap + `filesOrchestrator` (without file watching).
|
|
5307
5579
|
- Loads `src/config/*.ts`.
|
|
5308
5580
|
- Loads every module (`routes.ts`, `main.ts`, `events/*.ts`).
|
|
5309
|
-
- Starts **ALL** connectors, including HTTP.
|
|
5581
|
+
- Starts **ALL** connectors, including HTTP, by default.
|
|
5310
5582
|
|
|
5311
5583
|
Unlike the dev server, it doesn't watch files, doesn't do HMR, doesn't run health checkers. Just a working HTTP endpoint listening on the configured port.
|
|
5312
5584
|
|
|
@@ -5326,6 +5598,14 @@ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`
|
|
|
5326
5598
|
|
|
5327
5599
|
The request helpers follow with no extra wiring: the bound port is published as `WARLOCK_TEST_SERVER_PORT`, which the workers inherit, and `getTestServerUrl()` prefers it over their own `.env`-resolved `http.port`. `stopHttpTestServer()` withdraws it.
|
|
5328
5600
|
|
|
5601
|
+
### API-only suites — `startHttpTestServer({ web: false })`
|
|
5602
|
+
|
|
5603
|
+
```ts
|
|
5604
|
+
await startHttpTestServer({ web: false });
|
|
5605
|
+
```
|
|
5606
|
+
|
|
5607
|
+
Use this in `globalSetup` for a suite that exercises API routes only. It still boots the HTTP connector and every other configured connector, but excludes the optional `web` connector before its lifecycle begins. That means web's lazy delegate never constructs `WebConnector`, and Vite is never created. Omitting `web` keeps the default full-stack behavior.
|
|
5608
|
+
|
|
5329
5609
|
### The port is preflighted
|
|
5330
5610
|
|
|
5331
5611
|
Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
|
|
@@ -5381,8 +5661,8 @@ import { defineConfig } from "vitest/config";
|
|
|
5381
5661
|
export default defineConfig({
|
|
5382
5662
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
5383
5663
|
test: {
|
|
5384
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
5385
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
5664
|
+
globalSetup: "./src/test-global-setup.ts", // ← starts the HTTP server
|
|
5665
|
+
setupFiles: ["./src/test-setup.ts"], // ← setupTest + afterAll(teardownTest), per test file
|
|
5386
5666
|
environment: "node",
|
|
5387
5667
|
globals: false,
|
|
5388
5668
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -5418,7 +5698,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
|
|
|
5418
5698
|
```ts
|
|
5419
5699
|
import { getTestServerUrl } from "@warlock.js/core/tests";
|
|
5420
5700
|
|
|
5421
|
-
const url = getTestServerUrl();
|
|
5701
|
+
const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
|
|
5422
5702
|
```
|
|
5423
5703
|
|
|
5424
5704
|
Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config — unless `startHttpTestServer` published a port, which wins, since a worker's own config never sees the `{ port }` option passed in `globalSetup`. If you change the HTTP config, helpers follow automatically.
|
|
@@ -5456,9 +5736,9 @@ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
|
|
|
5456
5736
|
const body = await parseJsonResponse<MyShape>(response);
|
|
5457
5737
|
|
|
5458
5738
|
// Assert status + parse in one call
|
|
5459
|
-
const body = await expectJson<MyShape>(response);
|
|
5460
|
-
const body = await expectJson<MyShape>(response, 201);
|
|
5461
|
-
const body = await expectJson<MyShape>(response, 404);
|
|
5739
|
+
const body = await expectJson<MyShape>(response); // expects 200
|
|
5740
|
+
const body = await expectJson<MyShape>(response, 201); // expects 201
|
|
5741
|
+
const body = await expectJson<MyShape>(response, 404); // expects 404 (testing error paths)
|
|
5462
5742
|
```
|
|
5463
5743
|
|
|
5464
5744
|
`expectJson` throws with the actual status + response body when the assertion fails — no chasing "got 500" messages without context.
|
|
@@ -5494,7 +5774,7 @@ describe("Products API — happy path", () => {
|
|
|
5494
5774
|
|
|
5495
5775
|
```ts
|
|
5496
5776
|
it("rejects missing required fields with 400", async () => {
|
|
5497
|
-
const response = await testPost("/products", { name: "" });
|
|
5777
|
+
const response = await testPost("/products", { name: "" }); // price missing
|
|
5498
5778
|
const body = await expectJson<{ errors: Array<{ key: string }> }>(response, 400);
|
|
5499
5779
|
|
|
5500
5780
|
expect(body.errors.some((e) => e.key === "price")).toBe(true);
|
|
@@ -5619,13 +5899,13 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
|
|
|
5619
5899
|
|
|
5620
5900
|
⚠ **Corrected in 4.14.0 — `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of this skill through 4.13.0 said "per worker", the generated `src/test-setup.ts` carries a `Per-Worker Test Setup` comment saying the same thing, and **both were wrong.** Vitest runs `setupFiles` before **each test file**, and the setup module's registry is rebuilt every time — measured across all four `pool` × `isolate` combinations.
|
|
5621
5901
|
|
|
5622
|
-
⛔ **If your project was generated before 4.14.0, fix BOTH the comment and the call.** The comment is false, **and** the generated `setupTest({ connectors: true })` is now an
|
|
5902
|
+
⛔ **If your project was generated before 4.14.0, fix BOTH the comment and the call.** The comment is false, **and** the generated `setupTest({ connectors: true })` is now an _explicit_ value that overrides your `src/config/tests.ts`. **Bare `setupTest()` is the correct call.**
|
|
5623
5903
|
|
|
5624
5904
|
**The lifetime is FILE-SCOPED, on purpose.** Your setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing — correct under every pool, every isolation setting, and watch mode.**
|
|
5625
5905
|
|
|
5626
|
-
⚠ **A worker-scoped lifetime is possible and is deliberately not shipped yet.** Lifecycle state now lives in the worker runtime, so leaving the framework running would let every file in a worker share one bootstrap. Two things block claiming it: under `pool: "threads"` we cannot observe whether Node reclaims a torn-down thread's sockets and pools, and **in watch mode Vitest reuses workers between reruns, so there is no recycle and no cleanup owner.** It gets taken when the real cost is measured and the integration is chosen, not inherited. See
|
|
5906
|
+
⚠ **A worker-scoped lifetime is possible and is deliberately not shipped yet.** Lifecycle state now lives in the worker runtime, so leaving the framework running would let every file in a worker share one bootstrap. Two things block claiming it: under `pool: "threads"` we cannot observe whether Node reclaims a torn-down thread's sockets and pools, and **in watch mode Vitest reuses workers between reruns, so there is no recycle and no cleanup owner.** It gets taken when the real cost is measured and the integration is chosen, not inherited. See _Lifecycle and repeated calls_ below.
|
|
5627
5907
|
|
|
5628
|
-
⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with
|
|
5908
|
+
⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with _"has no exported member"_ — **add `/tests` to the specifier and nothing else changes.**
|
|
5629
5909
|
|
|
5630
5910
|
## The shape
|
|
5631
5911
|
|
|
@@ -5642,7 +5922,7 @@ describe("registerUserService", () => {
|
|
|
5642
5922
|
});
|
|
5643
5923
|
|
|
5644
5924
|
expect(user.get("email")).toBe("test@example.com");
|
|
5645
|
-
expect(user.get("password")).not.toBe("secret");
|
|
5925
|
+
expect(user.get("password")).not.toBe("secret"); // hashed by useHashedPassword()
|
|
5646
5926
|
|
|
5647
5927
|
const found = await usersRepository.first({ email: "test@example.com" });
|
|
5648
5928
|
expect(found).toBeDefined();
|
|
@@ -5667,18 +5947,18 @@ What it does (in order):
|
|
|
5667
5947
|
3. Runs `bootstrap()` — env, app, prestart hooks.
|
|
5668
5948
|
4. Initializes the `filesOrchestrator` (module/route/config discovery, no file watching).
|
|
5669
5949
|
5. Loads all `src/config/*.ts` files.
|
|
5670
|
-
6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See
|
|
5950
|
+
6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See _Selecting connectors_ below.
|
|
5671
5951
|
7. Starts the chosen connectors — but **never `http`** when you pass a boolean. HTTP is the global-setup's job.
|
|
5672
5952
|
|
|
5673
5953
|
The result: DB/cache/logger/storage connections your code can use. Models save, repositories query, services run. Same code as production, just isolated to the test process.
|
|
5674
5954
|
|
|
5675
5955
|
### The `connectors` parameter
|
|
5676
5956
|
|
|
5677
|
-
| Value
|
|
5678
|
-
|
|
|
5679
|
-
| `true`
|
|
5680
|
-
| `false`
|
|
5681
|
-
| `["database", "cache"]` | Just those, in that order
|
|
5957
|
+
| Value | Boots | Use when |
|
|
5958
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
5959
|
+
| `true` _(default)_ | Every connector except `http` — via `startWithout(["http"])` (db, cache, logger, storage, and socket if configured) | Most service / repository / model tests. |
|
|
5960
|
+
| `false` | None | Pure logic tests with no DB / cache touches (parsers, validators, util functions). |
|
|
5961
|
+
| `["database", "cache"]` | Just those, in that order | A test that only needs DB but not, say, the storage driver. |
|
|
5682
5962
|
|
|
5683
5963
|
The default `true` is the sane choice. Reach for `false` when the unit you're testing genuinely doesn't talk to any framework subsystem — pulling up a DB connection **for every file** just to test a string parser is wasted setup time.
|
|
5684
5964
|
|
|
@@ -5694,25 +5974,25 @@ export default testsConfigurations;
|
|
|
5694
5974
|
|
|
5695
5975
|
⚠ **BREAKING in 4.14.0 — the precedence flipped.**
|
|
5696
5976
|
|
|
5697
|
-
|
|
|
5698
|
-
|
|
5699
|
-
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true`
|
|
5700
|
-
| **4.14.0 onward**
|
|
5977
|
+
| | Order |
|
|
5978
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------- |
|
|
5979
|
+
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true` |
|
|
5980
|
+
| **4.14.0 onward** | **explicit `setupTest({ connectors })` parameter** **>** `tests.connectors` config **>** `true` |
|
|
5701
5981
|
|
|
5702
|
-
**An explicit call-site value now beats project config.** If your project sets `tests.connectors`
|
|
5982
|
+
**An explicit call-site value now beats project config.** If your project sets `tests.connectors` _and_ some test file passes `connectors` explicitly, **that file will start a different connector set after upgrading.** Search for `setupTest({` across your tests before you upgrade — a call passing `connectors` was previously ignored and is now honoured.
|
|
5703
5983
|
|
|
5704
5984
|
**"Explicit" means you supplied a non-`undefined` value.** Both of these fall through to config:
|
|
5705
5985
|
|
|
5706
5986
|
```ts
|
|
5707
|
-
await setupTest();
|
|
5708
|
-
await setupTest({});
|
|
5987
|
+
await setupTest(); // → tests.connectors, else true
|
|
5988
|
+
await setupTest({}); // → tests.connectors, else true
|
|
5709
5989
|
await setupTest({ connectors: undefined }); // → tests.connectors, else true — NOT "start none"
|
|
5710
5990
|
```
|
|
5711
5991
|
|
|
5712
5992
|
The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase your project config.
|
|
5713
5993
|
|
|
5714
5994
|
```ts
|
|
5715
|
-
await setupTest({ connectors: false });
|
|
5995
|
+
await setupTest({ connectors: false }); // → none, even if config says otherwise
|
|
5716
5996
|
await setupTest({ connectors: ["database"] }); // → exactly that, even if config differs
|
|
5717
5997
|
```
|
|
5718
5998
|
|
|
@@ -5739,7 +6019,7 @@ afterAll(teardownTest);
|
|
|
5739
6019
|
⛔ **Three things changed here in 4.14.0. If you generated this file earlier, replace all three — it is not a comment fix.**
|
|
5740
6020
|
|
|
5741
6021
|
1. **`afterAll(teardownTest)` is new and mandatory.** Nothing else closes the framework your tests started. This is what makes the lifetime file-scoped and owned rather than left running.
|
|
5742
|
-
2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an
|
|
6022
|
+
2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an _explicit_ value and would override `tests.connectors` for **every file in the project.** Bare means "whatever this project configured, else the default".
|
|
5743
6023
|
3. **The comment used to say `Per-Worker Test Setup` / "Runs in EACH Vitest worker thread".** False — see the top of this skill.
|
|
5744
6024
|
|
|
5745
6025
|
```ts title="vite.config.ts"
|
|
@@ -5750,8 +6030,8 @@ import { defineConfig } from "vitest/config";
|
|
|
5750
6030
|
export default defineConfig({
|
|
5751
6031
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
5752
6032
|
test: {
|
|
5753
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
5754
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
6033
|
+
globalSetup: "./src/test-global-setup.ts", // ← HTTP server (see test-http skill)
|
|
6034
|
+
setupFiles: ["./src/test-setup.ts"], // ← runs setupTest before EACH test file
|
|
5755
6035
|
environment: "node",
|
|
5756
6036
|
globals: false,
|
|
5757
6037
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -5797,9 +6077,9 @@ describe("createProductService", () => {
|
|
|
5797
6077
|
it("rejects duplicate names", async () => {
|
|
5798
6078
|
await Product.create({ name: "Existing", price: 10 });
|
|
5799
6079
|
|
|
5800
|
-
await expect(
|
|
5801
|
-
|
|
5802
|
-
)
|
|
6080
|
+
await expect(createProductService({ name: "Existing", price: 20 })).rejects.toThrow(
|
|
6081
|
+
/already exists/i,
|
|
6082
|
+
);
|
|
5803
6083
|
});
|
|
5804
6084
|
});
|
|
5805
6085
|
```
|
|
@@ -5869,7 +6149,7 @@ afterEach(async () => {
|
|
|
5869
6149
|
|
|
5870
6150
|
Vitest runs the tests within one file sequentially, so an `afterEach` truncate gives each test a clean slate.
|
|
5871
6151
|
|
|
5872
|
-
⚠ **Cross
|
|
6152
|
+
⚠ **Cross-_file_ isolation is not solved by this.** Separate workers get separate **connections**, not separate **rows** — two files pointed at the same database see each other's committed data regardless of pool or worker count. **Truncate what your file wrote; don't assume the worker boundary did it for you.** Real data isolation (DB-per-worker, transaction-per-test) is a separate piece of work and is not in this release.
|
|
5873
6153
|
|
|
5874
6154
|
### Skipping connectors for pure logic tests
|
|
5875
6155
|
|
|
@@ -5879,7 +6159,7 @@ import { setupTest } from "@warlock.js/core/tests";
|
|
|
5879
6159
|
import { slugify } from "../utils/slugify";
|
|
5880
6160
|
|
|
5881
6161
|
beforeAll(async () => {
|
|
5882
|
-
await setupTest({ connectors: false });
|
|
6162
|
+
await setupTest({ connectors: false }); // starts no connectors at all
|
|
5883
6163
|
});
|
|
5884
6164
|
|
|
5885
6165
|
describe("slugify", () => {
|
|
@@ -5906,7 +6186,7 @@ beforeAll(async () => {
|
|
|
5906
6186
|
});
|
|
5907
6187
|
|
|
5908
6188
|
afterAll(async () => {
|
|
5909
|
-
await teardownTest();
|
|
6189
|
+
await teardownTest(); // ← REQUIRED, see below
|
|
5910
6190
|
});
|
|
5911
6191
|
```
|
|
5912
6192
|
|
|
@@ -5927,16 +6207,16 @@ afterAll(async () => {
|
|
|
5927
6207
|
|
|
5928
6208
|
`setupTest` / `teardownTest` are a pair. **The harness that calls one owns calling the other in the same context.**
|
|
5929
6209
|
|
|
5930
|
-
| Call
|
|
5931
|
-
|
|
5932
|
-
| `setupTest(x)` while idle
|
|
5933
|
-
| `setupTest(x)` while already ready with the **same** effective options
|
|
5934
|
-
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested
|
|
5935
|
-
| two concurrent `setupTest(x)` calls
|
|
5936
|
-
| `setupTest` after a failed setup
|
|
5937
|
-
| `teardownTest()` while idle
|
|
5938
|
-
| two concurrent `teardownTest()` calls
|
|
5939
|
-
| `setupTest(y)` after a **successful** teardown
|
|
6210
|
+
| Call | Behaviour |
|
|
6211
|
+
| --------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
6212
|
+
| `setupTest(x)` while idle | bootstraps |
|
|
6213
|
+
| `setupTest(x)` while already ready with the **same** effective options | no-op |
|
|
6214
|
+
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested |
|
|
6215
|
+
| two concurrent `setupTest(x)` calls | share one startup |
|
|
6216
|
+
| `setupTest` after a failed setup | allowed — a failed setup unwinds and returns to idle |
|
|
6217
|
+
| `teardownTest()` while idle | no-op |
|
|
6218
|
+
| two concurrent `teardownTest()` calls | share one shutdown |
|
|
6219
|
+
| `setupTest(y)` after a **successful** teardown | allowed, different options fine |
|
|
5940
6220
|
|
|
5941
6221
|
**"Same options" is compared by meaning, not by literal value** — connector arrays are deduplicated and compared as sets, so `["cache", "database"]` and `["database", "cache", "cache"]` are the same selection.
|
|
5942
6222
|
|
|
@@ -5951,7 +6231,7 @@ afterAll(async () => {
|
|
|
5951
6231
|
```ts title="src/config/tests.ts"
|
|
5952
6232
|
const testsConfigurations = {
|
|
5953
6233
|
connectors: ["database", "logger"],
|
|
5954
|
-
setupTimeout: 120000,
|
|
6234
|
+
setupTimeout: 120000, // milliseconds — this is the default
|
|
5955
6235
|
};
|
|
5956
6236
|
|
|
5957
6237
|
export default testsConfigurations;
|
|
@@ -5970,9 +6250,9 @@ in `src/config/tests.ts` — milliseconds, default 120000.
|
|
|
5970
6250
|
1. **It bounds the setup ATTEMPT, not teardown separately.** `teardownTest()` awaits the same attempt, so it inherits the bound — **one timer, not two.** A second teardown-side deadline was tried and rejected: it re-introduced the unbounded re-entry this whole guard exists to remove.
|
|
5971
6251
|
2. **Expiry poisons the lifecycle**, it does not return to `idle`. The attempt may have started connectors nobody can now account for, so pretending the runtime is clean would be worse than refusing.
|
|
5972
6252
|
3. **⛔ An invalid `setupTimeout` throws, naming the bad value.** Zero, negative and non-numeric all fail loudly rather than falling back to the default — a silent fallback would hide a typo behind a working suite.
|
|
5973
|
-
4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens
|
|
6253
|
+
4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens _inside_ the window being bounded; re-arming naively would give you the default plus your configured value.
|
|
5974
6254
|
|
|
5975
|
-
⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default
|
|
6255
|
+
⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default _value_ is tested while the production timer — and whether its `unref` actually releases the worker — is not. **And no spec observes a real hang**: the stuck attempt is a mock gate, not a socket that never returns. These prove what the lifecycle _decides_, not what a genuinely wedged connector does.
|
|
5976
6256
|
|
|
5977
6257
|
### State is per worker runtime, not per module
|
|
5978
6258
|
|
|
@@ -6003,6 +6283,16 @@ In both cases the guard's scope matches the resource's scope, which is the point
|
|
|
6003
6283
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — where tests live in a module (`tests/*.test.ts`).
|
|
6004
6284
|
- [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — `warlock add test` for the initial scaffold.
|
|
6005
6285
|
|
|
6286
|
+
## App modules and `importModule`
|
|
6287
|
+
|
|
6288
|
+
Under Vitest, test files run through vite-node while Warlock's own native `import()` of app modules (config, routes, events) does not, so app classes such as `User` can exist twice in one worker and `instanceof` fails. Pass `importModule` to `setupTest` so Warlock loads app modules through the test's own module graph:
|
|
6289
|
+
|
|
6290
|
+
```ts
|
|
6291
|
+
setupTest({ importModule: (file) => import(file) });
|
|
6292
|
+
```
|
|
6293
|
+
|
|
6294
|
+
The generated `src/test-setup.ts` already does this. Without it, `setupTest()` under Vitest warns once.
|
|
6295
|
+
|
|
6006
6296
|
|
|
6007
6297
|
## update-packages `@warlock.js/core/update-packages/SKILL.md`
|
|
6008
6298
|
|
|
@@ -6764,7 +7054,7 @@ cors: {
|
|
|
6764
7054
|
|
|
6765
7055
|
---
|
|
6766
7056
|
name: use-localization
|
|
6767
|
-
description: 'Multi-locale translations via `groupedTranslations` (declare keys), `t()`
|
|
7057
|
+
description: 'Multi-locale translations via `groupedTranslations` (declare keys), `t()` (look up; always use it in app code, never `request.t()`), `request.getLocaleCode()` (detect locale from headers/query), `getLocalized` (pick the right value from a localized-array column). Triggers: `groupedTranslations`, `t`, `request.t`, `request.trans`, `request.transFrom`, `request.getLocaleCode`, `request.setLocaleCode`, `getLocalized`; "add a translation key", "resolve a localized error message", "detect request locale", "pick the right per-locale column value"; typical import `import { t, getLocalized } from "@warlock.js/core"`. Skip: resource output — `@warlock.js/core/define-resource/SKILL.md`; module scaffold — `@warlock.js/core/create-module/SKILL.md`; competing libs `i18next`, `react-intl`, raw `@mongez/localization`.'
|
|
6768
7058
|
---
|
|
6769
7059
|
|
|
6770
7060
|
# Warlock — translate keys + pick localized values
|
|
@@ -6779,7 +7069,7 @@ Both pivot on **the request's locale** — auto-detected from headers / query st
|
|
|
6779
7069
|
## The shape
|
|
6780
7070
|
|
|
6781
7071
|
```ts
|
|
6782
|
-
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.
|
|
7072
|
+
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.json)
|
|
6783
7073
|
import { groupedTranslations } from "@mongez/localization";
|
|
6784
7074
|
|
|
6785
7075
|
groupedTranslations("products", {
|
|
@@ -6791,8 +7081,7 @@ groupedTranslations("products", {
|
|
|
6791
7081
|
import { t } from "@warlock.js/core";
|
|
6792
7082
|
|
|
6793
7083
|
throw new ResourceNotFoundError(t("products.notFound"));
|
|
6794
|
-
|
|
6795
|
-
return response.success({ message: request.t("products.created") });
|
|
7084
|
+
return response.success({ message: t("products.created") });
|
|
6796
7085
|
|
|
6797
7086
|
// 3. Pick a value from a localized-array column
|
|
6798
7087
|
import { getLocalized } from "@warlock.js/core";
|
|
@@ -6801,7 +7090,26 @@ const name = getLocalized(product.get("name_translations"));
|
|
|
6801
7090
|
// → reads the current request's locale, returns the matching value
|
|
6802
7091
|
```
|
|
6803
7092
|
|
|
6804
|
-
## Declaring translations — `
|
|
7093
|
+
## Declaring translations — module `locales.json`
|
|
7094
|
+
|
|
7095
|
+
Prefer a data-only dictionary in `src/app/<module>/utils/locales.json`. The
|
|
7096
|
+
module folder is its default group, so this example registers
|
|
7097
|
+
`products.notFound`; a root `$group` string replaces that group when a module
|
|
7098
|
+
needs a different namespace. The module loader validates the strict JSON
|
|
7099
|
+
shape, flattens nested keys, and rejects duplicate ownership. Do not import the
|
|
7100
|
+
file yourself.
|
|
7101
|
+
|
|
7102
|
+
```json title="src/app/products/utils/locales.json"
|
|
7103
|
+
{
|
|
7104
|
+
"notFound": { "en": "Product not found", "ar": "المنتج غير موجود" },
|
|
7105
|
+
"errors": { "outOfStock": { "en": "Product out of stock", "ar": "المنتج غير متوفر" } }
|
|
7106
|
+
}
|
|
7107
|
+
```
|
|
7108
|
+
|
|
7109
|
+
`utils/locales.ts` with `groupedTranslations` remains supported but is
|
|
7110
|
+
deprecated and will be removed in v6. Migrate it with `warlock doctor --fix`.
|
|
7111
|
+
|
|
7112
|
+
### Legacy `groupedTranslations`
|
|
6805
7113
|
|
|
6806
7114
|
Every module owns its translation namespace under `src/app/<module>/utils/locales.ts`. The file is auto-loaded at boot — you don't import it from anywhere; the framework picks it up via the module loader.
|
|
6807
7115
|
|
|
@@ -6838,31 +7146,25 @@ t("products.outOfStock", { name: "Pen", count: 0 });
|
|
|
6838
7146
|
|
|
6839
7147
|
Placeholders use the `:name` syntax. The lookup helper substitutes them from the second argument.
|
|
6840
7148
|
|
|
6841
|
-
## Looking up translations —
|
|
7149
|
+
## Looking up translations — always `t()`
|
|
6842
7150
|
|
|
6843
|
-
|
|
7151
|
+
**Rule: app code translates with `t()` from `@warlock.js/core`.** It works everywhere: controllers, services, page and layout loaders, `metadata`, events and jobs. It returns a `string`.
|
|
6844
7152
|
|
|
6845
7153
|
```ts
|
|
6846
|
-
// 1. Top-level helper — works inside or outside a request.
|
|
6847
|
-
// Inside a request: uses request's locale.
|
|
6848
|
-
// Outside: uses the global default locale.
|
|
6849
7154
|
import { t } from "@warlock.js/core";
|
|
6850
7155
|
|
|
6851
7156
|
t("products.notFound");
|
|
6852
7157
|
t("products.outOfStock", { name: "Pen", count: 0 });
|
|
6853
|
-
|
|
6854
|
-
// 2. On the request object — explicit, scoped to that request.
|
|
6855
|
-
request.t("products.notFound");
|
|
6856
|
-
|
|
6857
|
-
// 3. Alias for request.t — same behavior.
|
|
6858
|
-
request.trans("products.notFound");
|
|
6859
7158
|
```
|
|
6860
7159
|
|
|
6861
|
-
|
|
7160
|
+
- Inside a request (a controller or a page loader) it uses the request's locale. In a page it first checks the route's `locales.json` snapshot, then falls back to module translations (`groupedTranslations` or module `locales.json`).
|
|
7161
|
+
- Outside a request (jobs, CLI) it uses the configured default locale.
|
|
7162
|
+
- `request.t()` / `request.trans()` are the per-request binding that `t()` delegates to. Don't call them in app code: `t()` gives the same result without threading `request` through services.
|
|
7163
|
+
- Don't wrap it in `String(...)`; it is already typed `string`.
|
|
6862
7164
|
|
|
6863
7165
|
### Web `useTrans()` key checking
|
|
6864
7166
|
|
|
6865
|
-
When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic groups/keys and placeholders are not inferred.
|
|
7167
|
+
When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from module `locales.json` dictionaries and literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic TypeScript groups/keys and placeholders are not inferred.
|
|
6866
7168
|
|
|
6867
7169
|
`useTrans()` now works correctly across hydration (5.15.0): the hydration
|
|
6868
7170
|
payload ships a `translations` key with the active locale's keywords, and
|
|
@@ -6908,7 +7210,7 @@ export default {
|
|
|
6908
7210
|
request.setLocaleCode("ar");
|
|
6909
7211
|
```
|
|
6910
7212
|
|
|
6911
|
-
After this, every `
|
|
7213
|
+
After this, every `t(...)` and `getLocalized(...)` in the same request returns Arabic. Useful for user-preference overrides (e.g. "this user has set their language to ar in their profile — switch the request's locale on auth").
|
|
6912
7214
|
|
|
6913
7215
|
## Localized columns — `getLocalized`
|
|
6914
7216
|
|
|
@@ -7005,6 +7307,7 @@ The error's message is locale-aware — the `inject-request-context` middleware
|
|
|
7005
7307
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
7006
7308
|
import { type CreateProductSchema } from "../schema/create-product.schema";
|
|
7007
7309
|
import { createProductService } from "../services/create-product.service";
|
|
7310
|
+
import { t } from "@warlock.js/core";
|
|
7008
7311
|
|
|
7009
7312
|
export const createProductController: GuardedRequestHandler<CreateProductSchema> = async ({
|
|
7010
7313
|
request,
|
|
@@ -7012,7 +7315,7 @@ export const createProductController: GuardedRequestHandler<CreateProductSchema>
|
|
|
7012
7315
|
}) => {
|
|
7013
7316
|
const product = await createProductService(request.validated());
|
|
7014
7317
|
return response.success({
|
|
7015
|
-
message:
|
|
7318
|
+
message: t("products.created"),
|
|
7016
7319
|
product,
|
|
7017
7320
|
});
|
|
7018
7321
|
};
|
|
@@ -7052,11 +7355,11 @@ await Product.create({
|
|
|
7052
7355
|
GET /products/42?locale=ar
|
|
7053
7356
|
```
|
|
7054
7357
|
|
|
7055
|
-
`request.getLocaleCode()` returns `"ar"` — every `
|
|
7358
|
+
`request.getLocaleCode()` returns `"ar"` — every `t(...)` and resource-output `getLocalized` returns Arabic. No app code changes required.
|
|
7056
7359
|
|
|
7057
7360
|
## Gotchas
|
|
7058
7361
|
|
|
7059
|
-
- **Locales files are auto-loaded — don't import them.** `src/app/<module>/utils/locales.
|
|
7362
|
+
- **Locales files are auto-loaded — don't import them.** `src/app/<module>/utils/locales.json` is picked up by the module loader on boot. Importing legacy `locales.ts` manually causes double-registration warnings.
|
|
7060
7363
|
- **Group names should match module URL slugs.** `groupedTranslations("products", ...)` for the `/products` module. Mismatch breaks the convention but doesn't throw.
|
|
7061
7364
|
- **Every key needs every locale.** If a key is missing for the active locale, the lookup falls back to the key string itself (`"products.notFound"` literally appears in the response). Always ship all locales together when adding a key.
|
|
7062
7365
|
- **`t()` outside a request uses the global default.** `Application.environment === "test"` or background jobs without a request context get the configured `app.localeCode`, not whatever the most recent request used.
|
|
@@ -7069,8 +7372,8 @@ GET /products/42?locale=ar
|
|
|
7069
7372
|
|
|
7070
7373
|
- [`send-response/SKILL.md`](../send-response/SKILL.md) — error helpers that pair with translated messages (`response.notFound({ error: t("...") })`).
|
|
7071
7374
|
- [`define-resource/SKILL.md`](../define-resource/SKILL.md) — using `getLocalized` inside resource output for clean per-locale responses.
|
|
7072
|
-
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.
|
|
7073
|
-
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.
|
|
7375
|
+
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.json` file is part of the generated module scaffold.
|
|
7376
|
+
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.json` is auto-loaded; the suffix is mandatory.
|
|
7074
7377
|
|
|
7075
7378
|
## Route-owned web translations (5.17)
|
|
7076
7379
|
|
|
@@ -7084,9 +7387,9 @@ GET /products/42?locale=ar
|
|
|
7084
7387
|
}
|
|
7085
7388
|
```
|
|
7086
7389
|
|
|
7087
|
-
This produces `account.title` and `account.actions.save`. Keep every leaf complete for the configured locale list. The page pipeline selects an immutable snapshot for its route source and locale. `useTrans()
|
|
7390
|
+
This produces `account.title` and `account.actions.save`. Keep every leaf complete for the configured locale list. The page pipeline selects an immutable snapshot for its route source and locale. `useTrans()` and `t()` use that snapshot; a missing scoped key returns the key itself. With no route-locales manifest, the existing global `@mongez/localization` registry remains the legacy fallback path.
|
|
7088
7391
|
|
|
7089
|
-
When a route-locales snapshot applies, `
|
|
7392
|
+
When a route-locales snapshot applies, `t()` (through the request binding) and `request.transFrom(locale, key, placeholders?)` resolve through it; without one, they retain the existing global-registry behavior. The underlying `transFromKeywords(locale, keywords, key, placeholders?, converter?)` is supplied by `@mongez/localization` 3.5.0. Error pages rebind to their own snapshot, while framework fallback uses the app/root snapshot. Development HMR add/edit/delete publishes a new snapshot, and the snapshot revision keeps page-cache entries from reusing old JSON. Generated `.warlock` typings contain the flattened route keys; do not maintain a second handwritten key union.
|
|
7090
7393
|
|
|
7091
7394
|
|
|
7092
7395
|
## use-middleware `@warlock.js/core/use-middleware/SKILL.md`
|
|
@@ -8385,10 +8688,10 @@ const updateAvatarSchema = v.object({
|
|
|
8385
8688
|
|
|
8386
8689
|
## What the framework sends on failure
|
|
8387
8690
|
|
|
8388
|
-
The framework calls `response.failedSchema(result)` which sends `
|
|
8691
|
+
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):
|
|
8389
8692
|
|
|
8390
8693
|
```jsonc
|
|
8391
|
-
//
|
|
8694
|
+
// 422 Unprocessable Entity
|
|
8392
8695
|
{
|
|
8393
8696
|
"errors": [
|
|
8394
8697
|
{ "input": "email", "error": "The email must be a valid email" },
|