@warlock.js/core 4.5.0 → 4.6.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 +27 -0
- package/esm/ai/src/agent/agent.mjs +126 -7
- package/esm/ai/src/agent/agent.mjs.map +1 -1
- package/esm/ai/src/agent/signature.mjs +57 -0
- package/esm/ai/src/agent/signature.mjs.map +1 -0
- package/esm/ai/src/agent/snapshot.mjs +101 -0
- package/esm/ai/src/agent/snapshot.mjs.map +1 -0
- package/esm/ai/src/ai.mjs +37 -1
- package/esm/ai/src/ai.mjs.map +1 -1
- package/esm/ai/src/errors/agent-drift-error.mjs +31 -0
- package/esm/ai/src/errors/agent-drift-error.mjs.map +1 -0
- package/esm/ai/src/errors/index.mjs +2 -0
- package/esm/ai/src/errors/planner-drift-error.mjs +33 -0
- package/esm/ai/src/errors/planner-drift-error.mjs.map +1 -0
- package/esm/ai/src/image/image-cost.mjs +55 -0
- package/esm/ai/src/image/image-cost.mjs.map +1 -0
- package/esm/ai/src/image/image.mjs +112 -0
- package/esm/ai/src/image/image.mjs.map +1 -0
- package/esm/ai/src/image/index.mjs +4 -0
- package/esm/ai/src/index.mjs +20 -0
- package/esm/ai/src/planner/planner-run.mjs +161 -6
- package/esm/ai/src/planner/planner-run.mjs.map +1 -1
- package/esm/ai/src/planner/planner.mjs +25 -1
- package/esm/ai/src/planner/planner.mjs.map +1 -1
- package/esm/ai/src/planner/snapshot.mjs +95 -0
- package/esm/ai/src/planner/snapshot.mjs.map +1 -0
- package/esm/ai/src/rag/hybrid/bm25.mjs +51 -0
- package/esm/ai/src/rag/hybrid/bm25.mjs.map +1 -0
- package/esm/ai/src/rag/hybrid/hybrid-rank.mjs +29 -0
- package/esm/ai/src/rag/hybrid/hybrid-rank.mjs.map +1 -0
- package/esm/ai/src/rag/hybrid/rrf.mjs +30 -0
- package/esm/ai/src/rag/hybrid/rrf.mjs.map +1 -0
- package/esm/ai/src/rag/index.mjs +11 -0
- package/esm/ai/src/rag/loaders/errors.mjs +25 -0
- package/esm/ai/src/rag/loaders/errors.mjs.map +1 -0
- package/esm/ai/src/rag/loaders/index.mjs +7 -0
- package/esm/ai/src/rag/loaders/load-html.mjs +138 -0
- package/esm/ai/src/rag/loaders/load-html.mjs.map +1 -0
- package/esm/ai/src/rag/loaders/load-pdf.mjs +150 -0
- package/esm/ai/src/rag/loaders/load-pdf.mjs.map +1 -0
- package/esm/ai/src/rag/loaders/load-text.mjs +60 -0
- package/esm/ai/src/rag/loaders/load-text.mjs.map +1 -0
- package/esm/ai/src/rag/loaders/load-web.mjs +89 -0
- package/esm/ai/src/rag/loaders/load-web.mjs.map +1 -0
- package/esm/ai/src/rag/store/pg-vector-store.mjs +328 -0
- package/esm/ai/src/rag/store/pg-vector-store.mjs.map +1 -0
- package/esm/ai/src/rag/transforms/multi-query.mjs +41 -0
- package/esm/ai/src/rag/transforms/multi-query.mjs.map +1 -0
- package/esm/ai/src/speech/index.mjs +3 -0
- package/esm/ai/src/speech/speech.mjs +122 -0
- package/esm/ai/src/speech/speech.mjs.map +1 -0
- package/esm/ai/src/supervisor/entries.mjs +2 -2
- package/esm/ai/src/supervisor/entries.mjs.map +1 -1
- package/esm/ai/src/transcribe/audio-input.mjs +84 -0
- package/esm/ai/src/transcribe/audio-input.mjs.map +1 -0
- package/esm/ai/src/transcribe/index.mjs +4 -0
- package/esm/ai/src/transcribe/transcribe.mjs +127 -0
- package/esm/ai/src/transcribe/transcribe.mjs.map +1 -0
- package/esm/ai-openai/src/image.mjs +5 -0
- package/esm/ai-openai/src/index.mjs +3 -0
- package/esm/ai-openai/src/sdk.mjs +3 -0
- package/esm/ai-openai/src/speech.mjs +5 -0
- package/esm/ai-openai/src/transcription.mjs +6 -0
- package/esm/ai-openai/src/utils/index.mjs +1 -0
- package/esm/ai-openai/src/utils/to-openai-messages.mjs +3 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs +42 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/connectors.check.mjs +28 -0
- package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/health.check.mjs +35 -0
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/index.mjs +25 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +90 -0
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +69 -0
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/routes.check.mjs +30 -0
- package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs +28 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -0
- package/esm/cli/commands/doctor/format-report.mjs +44 -0
- package/esm/cli/commands/doctor/format-report.mjs.map +1 -0
- package/esm/cli/commands/doctor/run-checks.mjs +50 -0
- package/esm/cli/commands/doctor/run-checks.mjs.map +1 -0
- package/esm/cli/commands/doctor.command.mjs +29 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/stubs.mjs +6 -8
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs +129 -0
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -0
- package/esm/cli/commands/routes/route-row.mjs +92 -0
- package/esm/cli/commands/routes/route-row.mjs.map +1 -0
- package/esm/cli/commands/routes/routes-command.action.mjs +43 -0
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -0
- package/esm/cli/commands/routes.command.mjs +51 -0
- package/esm/cli/commands/routes.command.mjs.map +1 -0
- package/esm/cli/commands/seed.command.mjs +5 -0
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs +4 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/config/config-getter.mjs +5 -5
- package/esm/config/config-getter.mjs.map +1 -1
- package/esm/config/config-loader.mjs +2 -2
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/access-connector.mjs +2 -2
- package/esm/connectors/access-connector.mjs.map +1 -1
- package/esm/connectors/ai-connector.mjs +2 -2
- package/esm/connectors/ai-connector.mjs.map +1 -1
- package/esm/connectors/cache-connector.mjs +2 -2
- package/esm/connectors/cache-connector.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs +2 -2
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs +2 -2
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +9 -7
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs +2 -2
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs +2 -2
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/notifications-connector.mjs +2 -2
- package/esm/connectors/notifications-connector.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +3 -3
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/database/index.d.mts +2 -1
- package/esm/database/index.mjs +1 -0
- package/esm/database/seed-command-action.mjs +44 -4
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/database/seeds/seed-records-table-migration.mjs +36 -0
- package/esm/database/seeds/seed-records-table-migration.mjs.map +1 -0
- package/esm/database/seeds/seeder.d.mts +22 -5
- package/esm/database/seeds/seeder.d.mts.map +1 -1
- package/esm/database/seeds/seeder.errors.d.mts +22 -0
- package/esm/database/seeds/seeder.errors.d.mts.map +1 -0
- package/esm/database/seeds/seeder.errors.mjs +29 -0
- package/esm/database/seeds/seeder.errors.mjs.map +1 -0
- package/esm/database/seeds/seeder.mjs.map +1 -1
- package/esm/database/seeds/seeders.manager.mjs +90 -11
- package/esm/database/seeds/seeders.manager.mjs.map +1 -1
- package/esm/database/seeds/types.d.mts +117 -1
- package/esm/database/seeds/types.d.mts.map +1 -1
- package/esm/database/seeds/utils.mjs +6 -1
- package/esm/database/seeds/utils.mjs.map +1 -1
- package/esm/dev-server/module-loader.d.mts +12 -0
- package/esm/dev-server/module-loader.d.mts.map +1 -1
- package/esm/dev-server/module-loader.mjs +30 -2
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/http/config.mjs +2 -2
- package/esm/http/config.mjs.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -2
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/health.d.mts +14 -0
- package/esm/http/health.d.mts.map +1 -1
- package/esm/http/health.mjs +16 -0
- package/esm/http/health.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +5 -5
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +2 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +4 -4
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/plugins.mjs +9 -9
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/response.mjs +5 -5
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.mjs +4 -4
- package/esm/http/server.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +2 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +28 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs +39 -0
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/contracts/index.d.mts +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts +87 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/repositories/index.d.mts +1 -1
- package/esm/repositories/repository.manager.d.mts +99 -1
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs +128 -0
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/router/router.d.mts +9 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +21 -1
- package/esm/router/router.mjs.map +1 -1
- package/esm/utils/paths.mjs +2 -2
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +2 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +427 -45
- package/llms.txt +4 -2
- package/package.json +9 -9
- package/skills/README.md +65 -0
- package/skills/health-checks/SKILL.md +13 -0
- package/skills/use-repository/SKILL.md +38 -1
- package/skills/warlock-doctor/SKILL.md +135 -0
- package/skills/warlock-routes/SKILL.md +91 -0
- package/skills/write-seeder/SKILL.md +143 -45
package/llms-full.txt
CHANGED
|
@@ -2376,6 +2376,19 @@ health.removeCheck("db"); // unregister later if needed
|
|
|
2376
2376
|
|
|
2377
2377
|
A check returns `boolean | Promise<boolean>`. **A thrown error counts as a failed check** (it's surfaced in the `checks` map + the 503, not logged — probes poll often, so a failure is a normal signal, not an error event). Keep checks cheap and fast; they run on every `/ready` poll.
|
|
2378
2378
|
|
|
2379
|
+
### Routes-registered readiness signal
|
|
2380
|
+
|
|
2381
|
+
A booted HTTP app that ends up with **zero routes** almost always means a route module failed to register silently — without this check it would 404 every request while reporting ready. `health.addRoutesRegisteredCheck(getRouteCount)` registers a `"routes"` check that fails while the count is `0`:
|
|
2382
|
+
|
|
2383
|
+
```ts
|
|
2384
|
+
import { health, router } from "@warlock.js/core";
|
|
2385
|
+
|
|
2386
|
+
health.addRoutesRegisteredCheck(() => router.routeCount());
|
|
2387
|
+
// pass a second arg to rename the check, e.g. "http-routes"
|
|
2388
|
+
```
|
|
2389
|
+
|
|
2390
|
+
The count is passed as a getter so the registry stays decoupled from the router. Apps with no HTTP surface simply never register it.
|
|
2391
|
+
|
|
2379
2392
|
## Graceful shutdown (request draining)
|
|
2380
2393
|
|
|
2381
2394
|
On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks → connectors in reverse priority**. The HTTP connector's teardown drains instead of dropping:
|
|
@@ -6362,7 +6375,7 @@ Rule of thumb: transformers are for **pure, deterministic** transforms of the ro
|
|
|
6362
6375
|
|
|
6363
6376
|
---
|
|
6364
6377
|
name: use-repository
|
|
6365
|
-
description: 'Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()
|
|
6378
|
+
description: 'Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()`, the active/cached/cursor variants, and the `filterBy`-aware aggregates `sum()`/`avg()`/`min()`/`max()`/`groupBy()`/`aggregate()`. Triggers: `RepositoryManager`, `FilterRules`, `RepositoryOptions`, `.list`, `.listCached`, `.find`, `.findCached`, `.create`, `.update`, `.delete`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.aggregate`, `simpleSelectColumns`; "create a repository", "filter rules for a list endpoint", "cursor vs page pagination", "cached vs uncached read", "sum/avg/group-by with filters"; typical import `import { RepositoryManager } from "@warlock.js/core"`. Skip: cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; use-case pipelines — `@warlock.js/core/write-use-case/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing libs `typeorm` Repository, `prisma.client.<model>`, `@nestjs/typeorm`.'
|
|
6366
6379
|
---
|
|
6367
6380
|
|
|
6368
6381
|
# Warlock — use a repository
|
|
@@ -6553,6 +6566,43 @@ await repo.countActive(options?);
|
|
|
6553
6566
|
await repo.countCached(options?);
|
|
6554
6567
|
```
|
|
6555
6568
|
|
|
6569
|
+
### Aggregation
|
|
6570
|
+
|
|
6571
|
+
```ts
|
|
6572
|
+
await repo.sum(field, options?); // → number
|
|
6573
|
+
await repo.avg(field, options?); // → number
|
|
6574
|
+
await repo.min(field, options?); // → number
|
|
6575
|
+
await repo.max(field, options?); // → number
|
|
6576
|
+
|
|
6577
|
+
await repo.groupBy<R>(fields, aggregates, options?); // → R[]
|
|
6578
|
+
await repo.aggregate<R>(aggregates, options?); // → R | null
|
|
6579
|
+
```
|
|
6580
|
+
|
|
6581
|
+
Every aggregate runs `applyOptionsToQuery(prepareOptions(options))` **first** — exactly like `count()` — so `filterBy` (and its operator-injection guard), `where`, scopes, and the rest of the repository options scope the aggregate before it executes. Pass the same filter inputs you'd pass to `list()`:
|
|
6582
|
+
|
|
6583
|
+
```ts
|
|
6584
|
+
// filtered scalar aggregates — the filterBy rules apply
|
|
6585
|
+
const paidTotal = await ordersRepository.sum("total", { status: "paid" });
|
|
6586
|
+
const avgTicket = await ordersRepository.avg("total", { status: "paid" });
|
|
6587
|
+
|
|
6588
|
+
// group-by + aggregate expressions (use the cascade $agg helpers)
|
|
6589
|
+
import { $agg } from "@warlock.js/cascade";
|
|
6590
|
+
|
|
6591
|
+
const byStatus = await ordersRepository.groupBy(
|
|
6592
|
+
"status",
|
|
6593
|
+
{ count: $agg.count(), total: $agg.sum("total") },
|
|
6594
|
+
{ paidAfter: "2026-01-01" }, // a filterBy key
|
|
6595
|
+
);
|
|
6596
|
+
|
|
6597
|
+
// one summary row over the whole filtered set (null when empty)
|
|
6598
|
+
const summary = await ordersRepository.aggregate(
|
|
6599
|
+
{ total: $agg.sum("total"), avg: $agg.avg("total") },
|
|
6600
|
+
{ status: "paid" },
|
|
6601
|
+
);
|
|
6602
|
+
```
|
|
6603
|
+
|
|
6604
|
+
`fields` accepts a string, a `string[]`, or a column map; `aggregates` is a map of result alias → aggregate expression. Because filters are applied first, you never have to re-implement `filterBy` logic for a stats endpoint — the same inputs scope the count *and* the sums.
|
|
6605
|
+
|
|
6556
6606
|
### Chunking
|
|
6557
6607
|
|
|
6558
6608
|
```ts
|
|
@@ -7185,6 +7235,240 @@ Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasM
|
|
|
7185
7235
|
- [`send-response/SKILL.md`](../send-response/SKILL.md) — the full Response helper surface.
|
|
7186
7236
|
|
|
7187
7237
|
|
|
7238
|
+
## warlock-doctor `@warlock.js/core/warlock-doctor/SKILL.md`
|
|
7239
|
+
|
|
7240
|
+
---
|
|
7241
|
+
name: warlock-doctor
|
|
7242
|
+
description: 'Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `yarn warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.'
|
|
7243
|
+
---
|
|
7244
|
+
|
|
7245
|
+
# Warlock — `warlock doctor`
|
|
7246
|
+
|
|
7247
|
+
`warlock doctor` is a read-only preflight. It boots the app far enough to introspect it — loads every config file and bootstrap code so routes and connectors register — but **starts no connectors**, so it never opens a database, cache, or socket connection. It then runs a set of checks and prints a grouped pass / warn / fail report.
|
|
7248
|
+
|
|
7249
|
+
```bash
|
|
7250
|
+
yarn warlock doctor
|
|
7251
|
+
```
|
|
7252
|
+
|
|
7253
|
+
```
|
|
7254
|
+
✓ routes: 42 registered
|
|
7255
|
+
✓ config: required sections present (app, http)
|
|
7256
|
+
✓ connectors: 3 registered, active: database, cache
|
|
7257
|
+
⚠ optional-peers: not installed → unavailable: sharp (image processing); redis (Redis cache driver)
|
|
7258
|
+
✓ health: liveness /health + readiness /ready registered
|
|
7259
|
+
✓ release-hygiene: package.json and CHANGELOG agree on 4.6.0
|
|
7260
|
+
|
|
7261
|
+
Summary: 5 ok, 1 warn, 0 fail
|
|
7262
|
+
```
|
|
7263
|
+
|
|
7264
|
+
## What it checks
|
|
7265
|
+
|
|
7266
|
+
The default set is `defaultDoctorChecks` (in registration order — runtime-surface checks first, release hygiene last):
|
|
7267
|
+
|
|
7268
|
+
| Check | `ok` | `warn` | `fail` |
|
|
7269
|
+
| --- | --- | --- | --- |
|
|
7270
|
+
| `routes` | ≥ 1 route registered | 0 routes (a route module likely failed to load) | — |
|
|
7271
|
+
| `config` | required sections (`app`, `http`) present | — | a required section is missing |
|
|
7272
|
+
| `connectors` | manager enumerable; reports registered + active set | — | (only if the probe itself throws) |
|
|
7273
|
+
| `optional-peers` | every known optional peer installed | a peer is missing → its feature is unavailable | — |
|
|
7274
|
+
| `health` | `/health` + `/ready` will be exposed | `http.health.enabled = false` (probes off) | — |
|
|
7275
|
+
| `release-hygiene` | `package.json` version matches the top `## x.y.z` CHANGELOG heading | no `CHANGELOG.md`, or no parseable heading | version ≠ top heading |
|
|
7276
|
+
|
|
7277
|
+
Optional peers are resolved from the **consuming app's** `node_modules` (via `createRequire(process.cwd())`), so the report reflects what *your* project has installed, not core's own deps.
|
|
7278
|
+
|
|
7279
|
+
## Exit code
|
|
7280
|
+
|
|
7281
|
+
```
|
|
7282
|
+
warn only → exit 0 (warnings never fail the command)
|
|
7283
|
+
any fail → exit 1 (so CI / a release script can gate on it)
|
|
7284
|
+
```
|
|
7285
|
+
|
|
7286
|
+
`doctor` itself can never crash: a check that throws is caught by the runner and recorded as a `fail` (detail `check threw: <message>`), not re-thrown.
|
|
7287
|
+
|
|
7288
|
+
## The contract
|
|
7289
|
+
|
|
7290
|
+
```ts
|
|
7291
|
+
type CheckStatus = "ok" | "warn" | "fail";
|
|
7292
|
+
|
|
7293
|
+
type CheckResult = {
|
|
7294
|
+
name: string; // stable check name, e.g. "routes"
|
|
7295
|
+
status: CheckStatus;
|
|
7296
|
+
detail: string; // one-line, user-facing explanation
|
|
7297
|
+
};
|
|
7298
|
+
|
|
7299
|
+
type DoctorCheck = {
|
|
7300
|
+
name: string;
|
|
7301
|
+
run: () => CheckResult | Promise<CheckResult>; // MUST be read-only
|
|
7302
|
+
};
|
|
7303
|
+
|
|
7304
|
+
type DoctorReport = {
|
|
7305
|
+
results: CheckResult[]; // in registration order
|
|
7306
|
+
summary: Record<CheckStatus, number>; // counts per status
|
|
7307
|
+
hasFailures: boolean; // true iff any fail
|
|
7308
|
+
exitCode: 0 | 1; // 1 when hasFailures, else 0
|
|
7309
|
+
};
|
|
7310
|
+
```
|
|
7311
|
+
|
|
7312
|
+
A `DoctorCheck.run` must be **read-only** — it may introspect the router, connectors, config, and `package.json`, but must never mutate state or open a connection. It may be sync or async, and may throw (the runner degrades a throw to a `fail`).
|
|
7313
|
+
|
|
7314
|
+
## Running checks yourself
|
|
7315
|
+
|
|
7316
|
+
The runner and formatter are exported, so you can assemble your own check set — e.g. an app-specific preflight script or a custom CLI command:
|
|
7317
|
+
|
|
7318
|
+
```ts
|
|
7319
|
+
import {
|
|
7320
|
+
runChecks,
|
|
7321
|
+
formatReportLines,
|
|
7322
|
+
printReport,
|
|
7323
|
+
defaultDoctorChecks,
|
|
7324
|
+
type DoctorCheck,
|
|
7325
|
+
} from "@warlock.js/core";
|
|
7326
|
+
|
|
7327
|
+
const migrationsCheck: DoctorCheck = {
|
|
7328
|
+
name: "migrations",
|
|
7329
|
+
async run() {
|
|
7330
|
+
const pending = await countPendingMigrations(); // your read-only probe
|
|
7331
|
+
|
|
7332
|
+
return pending === 0
|
|
7333
|
+
? { name: "migrations", status: "ok", detail: "no pending migrations" }
|
|
7334
|
+
: { name: "migrations", status: "warn", detail: `${pending} pending` };
|
|
7335
|
+
},
|
|
7336
|
+
};
|
|
7337
|
+
|
|
7338
|
+
const report = await runChecks([...defaultDoctorChecks, migrationsCheck]);
|
|
7339
|
+
|
|
7340
|
+
printReport(report); // colored, to stdout
|
|
7341
|
+
const lines = formatReportLines(report); // plain strings — snapshot-friendly
|
|
7342
|
+
|
|
7343
|
+
if (report.hasFailures) process.exit(report.exitCode);
|
|
7344
|
+
```
|
|
7345
|
+
|
|
7346
|
+
`formatReportLines` returns plain (uncolored) `<symbol> <name>: <detail>` lines plus a `Summary: …` line — handy for tests. `printReport` is the colored stdout wrapper; the structured `DoctorReport` stays the source of truth for the exit code.
|
|
7347
|
+
|
|
7348
|
+
## Patterns
|
|
7349
|
+
|
|
7350
|
+
### CI / pre-release gate
|
|
7351
|
+
|
|
7352
|
+
```bash
|
|
7353
|
+
yarn warlock doctor || exit 1 # non-zero exit fails the job
|
|
7354
|
+
```
|
|
7355
|
+
|
|
7356
|
+
A red `release-hygiene` line catches the classic "bumped `package.json` but forgot the CHANGELOG heading" mistake before a publish.
|
|
7357
|
+
|
|
7358
|
+
### Diagnosing a silent 404 surface
|
|
7359
|
+
|
|
7360
|
+
A `⚠ routes: 0 routes registered` line is the tell that a route module threw on import or registration and the failure was surfaced (not swallowed). Re-run `warlock dev` and read the boot error.
|
|
7361
|
+
|
|
7362
|
+
## Gotchas
|
|
7363
|
+
|
|
7364
|
+
- **`doctor` starts no connectors.** It intentionally does not open DB / cache / socket connections — the `connectors` check reports the *registered* set and which are already active, without forcing any up. Don't expect it to detect a down database.
|
|
7365
|
+
- **Optional-peers resolves from the project, not core.** A peer installed only inside core's own `node_modules` still reports as missing — the probe uses `process.cwd()`.
|
|
7366
|
+
- **Warnings don't fail the command.** Only a `fail` flips the exit code. Missing optional peers and disabled health endpoints are deliberate-choice `warn`s.
|
|
7367
|
+
- **Custom checks must stay read-only.** `run()` runs against a booted-but-not-connected app; mutating state or opening a connection breaks the contract and can hang the command.
|
|
7368
|
+
|
|
7369
|
+
## See also
|
|
7370
|
+
|
|
7371
|
+
- [`health-checks/SKILL.md`](../health-checks/SKILL.md) — the live `/health` + `/ready` HTTP probes and the `health` registry the `health` doctor check reports on.
|
|
7372
|
+
- [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — authoring a command + its `preload` plan, the shape `doctorCommand` is built from.
|
|
7373
|
+
- [`add-connector/SKILL.md`](../add-connector/SKILL.md) — connector registration, which the `connectors` check enumerates.
|
|
7374
|
+
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — project layout the checks assume.
|
|
7375
|
+
|
|
7376
|
+
|
|
7377
|
+
## warlock-routes `@warlock.js/core/warlock-routes/SKILL.md`
|
|
7378
|
+
|
|
7379
|
+
---
|
|
7380
|
+
name: warlock-routes
|
|
7381
|
+
description: 'Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Triggers: `warlock routes`, `routesCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI"; run as `yarn warlock routes`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.'
|
|
7382
|
+
---
|
|
7383
|
+
|
|
7384
|
+
# Warlock — `warlock routes`
|
|
7385
|
+
|
|
7386
|
+
`warlock routes` lists every registered HTTP route as a table. It's the read-only sibling of [`warlock doctor`](../warlock-doctor/SKILL.md): it boots the app far enough to register route modules — but **starts no connectors**, so it never opens a database, cache, or socket connection.
|
|
7387
|
+
|
|
7388
|
+
```bash
|
|
7389
|
+
yarn warlock routes
|
|
7390
|
+
```
|
|
7391
|
+
|
|
7392
|
+
```
|
|
7393
|
+
METHOD PATH NAME ACTION MW SOURCE
|
|
7394
|
+
GET /users users.list index 2 app/users/routes.ts
|
|
7395
|
+
POST /users users.create store 2 app/users/routes.ts
|
|
7396
|
+
GET /users/:id users.get show 2 app/users/routes.ts
|
|
7397
|
+
DELETE /users/:id users.delete destroy 3 app/users/routes.ts
|
|
7398
|
+
|
|
7399
|
+
4 routes (2 GET · 1 POST · 1 DELETE)
|
|
7400
|
+
```
|
|
7401
|
+
|
|
7402
|
+
The `METHOD` column is verb-colored (GET green, POST blue, PUT/PATCH yellow, DELETE red, `all`/OPTIONS/HEAD dim). Rows sort by path, then by HTTP-method order within a path.
|
|
7403
|
+
|
|
7404
|
+
## Columns
|
|
7405
|
+
|
|
7406
|
+
| Column | Meaning |
|
|
7407
|
+
| -------- | ----------------------------------------------------------------------------- |
|
|
7408
|
+
| `METHOD` | HTTP verb; a wildcard `all` route lists as `ALL`. |
|
|
7409
|
+
| `PATH` | Full request path (group prefix already folded in). |
|
|
7410
|
+
| `NAME` | Route name, or `—` when unnamed. |
|
|
7411
|
+
| `ACTION` | Handler function name (`anonymous` for an unnamed handler). |
|
|
7412
|
+
| `MW` | Count of middleware attached to the route. |
|
|
7413
|
+
| `SOURCE` | Source file the route registered from, or `—`. |
|
|
7414
|
+
|
|
7415
|
+
## Filters
|
|
7416
|
+
|
|
7417
|
+
Optional, case-insensitive, AND-combined:
|
|
7418
|
+
|
|
7419
|
+
```bash
|
|
7420
|
+
yarn warlock routes --method GET # -m exact HTTP method
|
|
7421
|
+
yarn warlock routes --path /users # -p path substring
|
|
7422
|
+
yarn warlock routes --name users # -n route-name substring
|
|
7423
|
+
yarn warlock routes -m POST -p /users
|
|
7424
|
+
```
|
|
7425
|
+
|
|
7426
|
+
## JSON output
|
|
7427
|
+
|
|
7428
|
+
`--json` (`-j`) emits the normalized rows instead of the table — for `jq`, a CI diff, or a generated API map. Filters apply before serialization.
|
|
7429
|
+
|
|
7430
|
+
```bash
|
|
7431
|
+
yarn warlock routes --json
|
|
7432
|
+
```
|
|
7433
|
+
|
|
7434
|
+
```json
|
|
7435
|
+
[
|
|
7436
|
+
{ "method": "GET", "path": "/users", "name": "users.list", "action": "index", "middleware": 2, "source": "app/users/routes.ts" }
|
|
7437
|
+
]
|
|
7438
|
+
```
|
|
7439
|
+
|
|
7440
|
+
## Patterns
|
|
7441
|
+
|
|
7442
|
+
### Audit which routes are guarded
|
|
7443
|
+
|
|
7444
|
+
```bash
|
|
7445
|
+
yarn warlock routes --json | jq '[.[] | select(.middleware == 0)]'
|
|
7446
|
+
```
|
|
7447
|
+
|
|
7448
|
+
Surfaces public routes (no middleware) — a quick check that auth-protected paths actually carry a guard.
|
|
7449
|
+
|
|
7450
|
+
### Confirm a route registered
|
|
7451
|
+
|
|
7452
|
+
```bash
|
|
7453
|
+
yarn warlock routes --name users.create
|
|
7454
|
+
```
|
|
7455
|
+
|
|
7456
|
+
An empty result means the route isn't registered — re-run `warlock dev` and read the boot error (the route-module loader is fail-loud, so a throwing route file aborts boot rather than being silently dropped).
|
|
7457
|
+
|
|
7458
|
+
## Gotchas
|
|
7459
|
+
|
|
7460
|
+
- **No connectors are started.** The list reflects what's *registered*, not what would connect. It never opens a DB/cache/socket.
|
|
7461
|
+
- **An empty table is a tell.** `No routes registered` is the same signal `warlock doctor`'s `routes` check warns on — a route module likely failed to load.
|
|
7462
|
+
- **`MW` counts, it doesn't name.** It's a "is this route guarded?" signal; for the actual chain, read the route definition.
|
|
7463
|
+
- **`ACTION` is the handler function name.** A controller method shows its method name; an inline anonymous handler shows `anonymous`.
|
|
7464
|
+
|
|
7465
|
+
## See also
|
|
7466
|
+
|
|
7467
|
+
- [`warlock-doctor/SKILL.md`](../warlock-doctor/SKILL.md) — the read-only diagnostics sibling; its `routes` check warns when this table would be empty.
|
|
7468
|
+
- [`register-route/SKILL.md`](../register-route/SKILL.md) — defining, naming, and grouping the routes this command lists.
|
|
7469
|
+
- [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — the command + `preload` shape `routesCommand` is built from.
|
|
7470
|
+
|
|
7471
|
+
|
|
7188
7472
|
## wire-socket `@warlock.js/core/wire-socket/SKILL.md`
|
|
7189
7473
|
|
|
7190
7474
|
---
|
|
@@ -7994,7 +8278,7 @@ export const optionalAuth: Middleware = async (request, response) => {
|
|
|
7994
8278
|
|
|
7995
8279
|
---
|
|
7996
8280
|
name: write-seeder
|
|
7997
|
-
description: 'Author a seed file under `src/app/<module>/seeds/<name>.ts` using the `seeder()` factory — `name`, `dependsOn`, `once`, `order`, `batchSize`, `run()`. Auto-discovered by `warlock seed`; tracked in a `seeds` table. Triggers: `seeder`, `Seeder`, `SeedResult`, `SeedersManager`, `warlock seed`, `--fresh`, `--list`, `--path`; "seed default roles", "one-time data migration", "auto-discovered seeds", "order seeds by dependency"; typical import `import { seeder } from "@warlock.js/core"`. Skip: module folder layout — `@warlock.js/core/create-module/SKILL.md`; repository CRUD — `@warlock.js/core/use-repository/SKILL.md`; CLI flags — `@warlock.js/core/write-cli-command/SKILL.md`; competing patterns: hand-rolled `node scripts/seed.js`, `typeorm-seeding`.'
|
|
8281
|
+
description: 'Author a seed file under `src/app/<module>/seeds/<name>.ts` using the `seeder()` factory — `name`, `dependsOn`, `once`, `order`, `batchSize`, `run({ track, now, batchSize })`. Auto-discovered by `warlock seed`; tracked in a `seeds` table; per-record refs in `seed_records` so `warlock seed --drop` can undo a seed. Triggers: `seeder`, `Seeder`, `SeedResult`, `SeedContext`, `SeedClock`, `track`, `now`, `batchSize`, `SeedersManager`, `warlock seed`, `--fresh`, `--drop`, `--list`, `--path`; "seed default roles", "undo a seed", "one-time data migration", "auto-discovered seeds", "order seeds by dependency", "deterministic seed timestamps", "inject a seed clock"; typical import `import { seeder } from "@warlock.js/core"`. Skip: module folder layout — `@warlock.js/core/create-module/SKILL.md`; repository CRUD — `@warlock.js/core/use-repository/SKILL.md`; CLI flags — `@warlock.js/core/write-cli-command/SKILL.md`; competing patterns: hand-rolled `node scripts/seed.js`, `typeorm-seeding`.'
|
|
7998
8282
|
---
|
|
7999
8283
|
|
|
8000
8284
|
# Warlock — write a seeder
|
|
@@ -8012,22 +8296,21 @@ export default seeder({
|
|
|
8012
8296
|
description: "Insert admin / member / viewer roles",
|
|
8013
8297
|
once: true,
|
|
8014
8298
|
order: 1,
|
|
8015
|
-
async run() {
|
|
8016
|
-
let recordsCreated = 0;
|
|
8017
|
-
|
|
8299
|
+
async run({ track }) {
|
|
8018
8300
|
for (const slug of ["admin", "member", "viewer"]) {
|
|
8019
8301
|
const existing = await Role.first({ slug });
|
|
8020
8302
|
if (existing) continue;
|
|
8021
8303
|
|
|
8022
|
-
|
|
8023
|
-
|
|
8304
|
+
// track() records the created row in `seed_records` (same transaction)
|
|
8305
|
+
// AND returns its argument so you can chain inline.
|
|
8306
|
+
track(await Role.create({ slug, name: slug.toUpperCase() }));
|
|
8024
8307
|
}
|
|
8025
|
-
|
|
8026
|
-
return { recordsCreated };
|
|
8027
8308
|
},
|
|
8028
8309
|
});
|
|
8029
8310
|
```
|
|
8030
8311
|
|
|
8312
|
+
`recordsCreated` is **auto-derived from the track count** — you no longer have to count by hand or return a `SeedResult`.
|
|
8313
|
+
|
|
8031
8314
|
Run them:
|
|
8032
8315
|
|
|
8033
8316
|
```bash
|
|
@@ -8035,10 +8318,14 @@ yarn warlock seed # discover + run all
|
|
|
8035
8318
|
yarn warlock seed --list # show registry, don't run
|
|
8036
8319
|
yarn warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
|
|
8037
8320
|
yarn warlock seed --fresh # truncate every table first, then run all
|
|
8321
|
+
yarn warlock seed --drop # undo every tracked record, reset the log
|
|
8322
|
+
yarn warlock seed --drop=default-roles # undo just one seeder's records
|
|
8038
8323
|
```
|
|
8039
8324
|
|
|
8040
8325
|
`--fresh` truncates **every** table in the DB (`datasource.driver.truncateTable(table, { cascade: true })`), including the `seeds` tracking table. After `--fresh`, `once: true` seeds will run again.
|
|
8041
8326
|
|
|
8327
|
+
`--drop` is the surgical alternative: it deletes only the rows a seeder **tracked**, in reverse run-order, then resets the matching `seeds`-log rows so `once: true` seeds re-run. See [Undo — `seed_records` + `--drop`](#undo--seed_records----drop).
|
|
8328
|
+
|
|
8042
8329
|
## The `Seeder` type
|
|
8043
8330
|
|
|
8044
8331
|
```ts
|
|
@@ -8050,15 +8337,69 @@ type Seeder = {
|
|
|
8050
8337
|
once?: boolean; // skip if a row exists in the `seeds` table
|
|
8051
8338
|
order?: number; // sort key — lower runs first
|
|
8052
8339
|
batchSize?: number; // documented; not enforced by the manager
|
|
8053
|
-
run(): Promise<SeedResult | void>;
|
|
8340
|
+
run(ctx: SeedContext): Promise<SeedResult | void>; // your work
|
|
8341
|
+
};
|
|
8342
|
+
|
|
8343
|
+
type SeedContext = {
|
|
8344
|
+
track: Track; // register created records for --drop
|
|
8345
|
+
now: SeedClock; // injectable clock — () => Date, default () => new Date()
|
|
8346
|
+
batchSize: number; // from the seeder's batchSize (0 when unset)
|
|
8054
8347
|
};
|
|
8055
8348
|
|
|
8349
|
+
type SeedClock = () => Date;
|
|
8350
|
+
|
|
8056
8351
|
type SeedResult = {
|
|
8057
|
-
recordsCreated: number; //
|
|
8352
|
+
recordsCreated: number; // OPTIONAL fallback when nothing was tracked
|
|
8058
8353
|
};
|
|
8059
8354
|
```
|
|
8060
8355
|
|
|
8061
|
-
|
|
8356
|
+
`run` now receives a `{ track, now, batchSize }` context. Declaring the parameter is optional — an existing **zero-arg `run()` keeps working unchanged** (it just ignores the context). Returning a `SeedResult` is also optional: the manager prefers the track count and only falls back to `result.recordsCreated` when you tracked nothing. A seed that neither tracks nor returns still counts as run (the `seeds` row is inserted with `recordsCreated: 0`).
|
|
8357
|
+
|
|
8358
|
+
## `now` — the injectable clock
|
|
8359
|
+
|
|
8360
|
+
`now` is a `SeedClock` (`() => Date`) the seed should read **instead of** an ambient `new Date()` / `dayjs()`. It defaults to `() => new Date()`, and the `SeedersManager` reads the **same clock** for its own seeds-log timestamps (`createdAt` / `firstRunAt` / `lastRunAt`) — so seed data and the tracking row never disagree.
|
|
8361
|
+
|
|
8362
|
+
```ts
|
|
8363
|
+
async run({ track, now }) {
|
|
8364
|
+
// every timestamp in the run reads from one clock
|
|
8365
|
+
track(await User.create({ name: "Admin", created_at: now() }));
|
|
8366
|
+
}
|
|
8367
|
+
```
|
|
8368
|
+
|
|
8369
|
+
Inject a fixed clock for deterministic tests or historical back-fills — pass `clock` to `SeedersManager` (`new SeedersManager({ clock: () => new Date("2020-01-01") })`) or, programmatically, to `seedCommandAction(options, { clock })`. Pinning the clock makes every timestamp in the run — seed data *and* the `seeds` log — resolve to that one value.
|
|
8370
|
+
|
|
8371
|
+
## `batchSize` — bulk-insert hint
|
|
8372
|
+
|
|
8373
|
+
`batchSize` surfaces the seeder's own `batchSize` field into the run context (`0` when unset), so a seed can forward it straight to `Model.createMany(rows, { batchSize })` without re-reading its config:
|
|
8374
|
+
|
|
8375
|
+
```ts
|
|
8376
|
+
async run({ track, batchSize }) {
|
|
8377
|
+
const rows = buildManyRows();
|
|
8378
|
+
track(await User.createMany(rows, { batchSize })); // chunked insert
|
|
8379
|
+
}
|
|
8380
|
+
```
|
|
8381
|
+
|
|
8382
|
+
The manager doesn't act on `batchSize` itself — it only hands the value through; the chunking happens inside `createMany`.
|
|
8383
|
+
|
|
8384
|
+
## `track` — register created records
|
|
8385
|
+
|
|
8386
|
+
`track` is how a seed tells the framework which rows it created, so `--drop` can undo them. It has three forms, and **every form returns its first argument** so you can wrap a `create()` call inline:
|
|
8387
|
+
|
|
8388
|
+
```ts
|
|
8389
|
+
track(model) // single model — reads model.getTableName() + model.id
|
|
8390
|
+
track([modelA, modelB]) // bulk — array of models
|
|
8391
|
+
track("legacy_table", id) // raw escape hatch — table name + id, no model
|
|
8392
|
+
```
|
|
8393
|
+
|
|
8394
|
+
```ts
|
|
8395
|
+
async run({ track }) {
|
|
8396
|
+
const role = track(await Role.create({ slug: "admin" })); // chain inline
|
|
8397
|
+
const users = track(await User.createMany([...])); // bulk
|
|
8398
|
+
track("audit_log", auditId); // raw
|
|
8399
|
+
}
|
|
8400
|
+
```
|
|
8401
|
+
|
|
8402
|
+
Tracked refs are written to the `seed_records` table **inside the same transaction** the seed runs in — if the seed throws, the refs roll back with the data. Only the **last run's** refs are kept: re-running a seed drops its prior refs first, so undo stays bounded to what currently exists.
|
|
8062
8403
|
|
|
8063
8404
|
## Auto-discovery
|
|
8064
8405
|
|
|
@@ -8102,6 +8443,34 @@ Skip-on-`once`:
|
|
|
8102
8443
|
|
|
8103
8444
|
Use `once: true` for irreversible "starter" data (the framework's default `roles`, fixed enum tables) and leave it off for repeatable seeds (dev fixtures you re-run after rolling the DB).
|
|
8104
8445
|
|
|
8446
|
+
## Undo — `seed_records` + `--drop`
|
|
8447
|
+
|
|
8448
|
+
Every record you `track()` is written to a `seed_records` table (created on first run via `SeedRecordsTableMigration`):
|
|
8449
|
+
|
|
8450
|
+
| Column | Meaning |
|
|
8451
|
+
| ---------- | ---------------------------------------- |
|
|
8452
|
+
| `seeder` | Owning seeder's `name` |
|
|
8453
|
+
| `table` | Table the tracked record lives in |
|
|
8454
|
+
| `recordId` | Primary key of the tracked record |
|
|
8455
|
+
| `runAt` | Wall time the reference was recorded |
|
|
8456
|
+
|
|
8457
|
+
`warlock seed --drop` reads those refs and undoes the seed:
|
|
8458
|
+
|
|
8459
|
+
```bash
|
|
8460
|
+
yarn warlock seed --drop # undo every tracked record across all seeders
|
|
8461
|
+
yarn warlock seed --drop=default-roles # undo just one seeder's records
|
|
8462
|
+
```
|
|
8463
|
+
|
|
8464
|
+
What it does, inside a single transaction:
|
|
8465
|
+
|
|
8466
|
+
1. Deletes the tracked records in **reverse run-order**, and within each seed in **reverse insertion-order** (highest `seed_records.id` first — so children created after their parents are removed first).
|
|
8467
|
+
2. Clears the `seed_records` refs it acted on.
|
|
8468
|
+
3. Resets the matching `seeds`-log rows, so a `once: true` seed **re-runs** on the next `warlock seed`.
|
|
8469
|
+
|
|
8470
|
+
Only **tracked** rows are deleted — records you created without `track()` (or rows added by hand) are untouched. A seed whose `run()` never calls `track()` can't be undone with `--drop`; it has nothing to delete and its log row stays.
|
|
8471
|
+
|
|
8472
|
+
`--drop` vs `--fresh`: `--fresh` is the blunt reset (truncates *every* table); `--drop` is surgical (only what a seed tracked). Reach for `--drop` when you want to re-run one seeder without nuking the whole DB.
|
|
8473
|
+
|
|
8105
8474
|
## Ordering
|
|
8106
8475
|
|
|
8107
8476
|
```ts
|
|
@@ -8120,19 +8489,34 @@ seeder({
|
|
|
8120
8489
|
|
|
8121
8490
|
The manager sorts by `order` ascending — lower runs first. Seeds without an `order` get sorted to the end (`Number.MAX_SAFE_INTEGER`).
|
|
8122
8491
|
|
|
8123
|
-
|
|
8492
|
+
### `dependsOn` — topological order
|
|
8493
|
+
|
|
8494
|
+
`dependsOn: ["user-roles"]` is **resolved** by the manager: it topologically sorts seeders so every dependency runs before its dependents, *layered over* the numeric `order` tie-break. Among seeders whose dependencies are already satisfied, the lowest `order` (then registration order) runs first.
|
|
8495
|
+
|
|
8496
|
+
```ts
|
|
8497
|
+
seeder({ name: "roles", order: 20, async run() { /* ... */ } });
|
|
8498
|
+
seeder({ name: "admin-user", dependsOn: ["roles"], order: 10, async run() { /* ... */ } });
|
|
8499
|
+
// → roles runs FIRST despite its higher order, because admin-user depends on it.
|
|
8500
|
+
```
|
|
8501
|
+
|
|
8502
|
+
Two ways it fails loudly:
|
|
8503
|
+
|
|
8504
|
+
- **Unknown dependency** — `dependsOn` names a seeder that isn't registered (or is `enabled: false`, since disabled seeds are filtered out *before* resolution) → `UnknownSeederDependencyError`.
|
|
8505
|
+
- **Cycle** — `a → b → a` → `SeederDependencyCycleError` (the message includes the cycle path).
|
|
8506
|
+
|
|
8507
|
+
Use `dependsOn` to express *hard* ordering constraints and `order` for soft sequencing within a dependency layer.
|
|
8124
8508
|
|
|
8125
8509
|
## Transactions
|
|
8126
8510
|
|
|
8127
|
-
By default, each seed runs inside a transaction:
|
|
8511
|
+
By default, each seed runs inside a transaction. The manager wraps `run({ track })` **and** the write of its tracked `seed_records` refs in one transaction:
|
|
8128
8512
|
|
|
8129
8513
|
```ts
|
|
8130
8514
|
const result = withTransaction
|
|
8131
|
-
? await transaction(
|
|
8132
|
-
: await
|
|
8515
|
+
? await transaction(runSeeder) // run() + seed_records writes, atomically
|
|
8516
|
+
: await runSeeder();
|
|
8133
8517
|
```
|
|
8134
8518
|
|
|
8135
|
-
If your `run()` throws, the transaction rolls back. The CLI takes `--transaction` to flip this — pass `--transaction=false` for large seeds where you want each insert committed individually (avoid the rollback-on-failure cost; accept partial state on error).
|
|
8519
|
+
If your `run()` throws, the transaction rolls back — including the `seed_records` refs, so a failed seed leaves no dangling track rows. The CLI takes `--transaction` to flip this — pass `--transaction=false` for large seeds where you want each insert committed individually (avoid the rollback-on-failure cost; accept partial state on error).
|
|
8136
8520
|
|
|
8137
8521
|
Within a single seed, multiple model `create()` / `insert()` calls share the same transaction — partial-on-error doesn't apply unless you opt out.
|
|
8138
8522
|
|
|
@@ -8148,7 +8532,7 @@ export default seeder({
|
|
|
8148
8532
|
name: "default-currencies",
|
|
8149
8533
|
description: "USD / EUR / SAR / EGP currencies",
|
|
8150
8534
|
order: 5,
|
|
8151
|
-
async run() {
|
|
8535
|
+
async run({ track }) {
|
|
8152
8536
|
const seeds = [
|
|
8153
8537
|
{ code: "USD", name: "US Dollar", decimals: 2 },
|
|
8154
8538
|
{ code: "EUR", name: "Euro", decimals: 2 },
|
|
@@ -8156,15 +8540,11 @@ export default seeder({
|
|
|
8156
8540
|
{ code: "EGP", name: "Egyptian Pound", decimals: 2 },
|
|
8157
8541
|
];
|
|
8158
8542
|
|
|
8159
|
-
let recordsCreated = 0;
|
|
8160
8543
|
for (const data of seeds) {
|
|
8161
8544
|
const existing = await Currency.first({ code: data.code });
|
|
8162
8545
|
if (existing) continue;
|
|
8163
|
-
await Currency.create(data);
|
|
8164
|
-
recordsCreated++;
|
|
8546
|
+
track(await Currency.create(data)); // count auto-derived from track()
|
|
8165
8547
|
}
|
|
8166
|
-
|
|
8167
|
-
return { recordsCreated };
|
|
8168
8548
|
},
|
|
8169
8549
|
});
|
|
8170
8550
|
```
|
|
@@ -8182,23 +8562,23 @@ export default seeder({
|
|
|
8182
8562
|
name: "default-admin",
|
|
8183
8563
|
description: "Insert the seed admin user",
|
|
8184
8564
|
once: true, // never re-run
|
|
8185
|
-
|
|
8186
|
-
async run() {
|
|
8565
|
+
dependsOn: ["default-roles"], // roles must exist first
|
|
8566
|
+
async run({ track }) {
|
|
8187
8567
|
const adminRole = await Role.first({ slug: "admin" });
|
|
8188
|
-
if (!adminRole) throw new Error("admin role missing — run
|
|
8189
|
-
|
|
8190
|
-
await User.create({
|
|
8191
|
-
email: "admin@example.com",
|
|
8192
|
-
password: "change-me-immediately",
|
|
8193
|
-
role_id: adminRole.id,
|
|
8194
|
-
});
|
|
8568
|
+
if (!adminRole) throw new Error("admin role missing — run the roles seed first");
|
|
8195
8569
|
|
|
8196
|
-
|
|
8570
|
+
track(
|
|
8571
|
+
await User.create({
|
|
8572
|
+
email: "admin@example.com",
|
|
8573
|
+
password: "change-me-immediately",
|
|
8574
|
+
role_id: adminRole.id,
|
|
8575
|
+
}),
|
|
8576
|
+
);
|
|
8197
8577
|
},
|
|
8198
8578
|
});
|
|
8199
8579
|
```
|
|
8200
8580
|
|
|
8201
|
-
Combining `once: true` + `
|
|
8581
|
+
Combining `once: true` + `dependsOn` is the canonical "starter data" pattern.
|
|
8202
8582
|
|
|
8203
8583
|
### Bulk dev fixtures (no `once`, large batch)
|
|
8204
8584
|
|
|
@@ -8211,16 +8591,15 @@ export default seeder({
|
|
|
8211
8591
|
description: "100 sample products for dev",
|
|
8212
8592
|
enabled: process.env.NODE_ENV === "development",
|
|
8213
8593
|
order: 100,
|
|
8214
|
-
async run() {
|
|
8215
|
-
let recordsCreated = 0;
|
|
8594
|
+
async run({ track }) {
|
|
8216
8595
|
for (let i = 0; i < 100; i++) {
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8596
|
+
track(
|
|
8597
|
+
await Product.create({
|
|
8598
|
+
name: `Sample ${i}`,
|
|
8599
|
+
price: Math.random() * 100,
|
|
8600
|
+
}),
|
|
8601
|
+
);
|
|
8222
8602
|
}
|
|
8223
|
-
return { recordsCreated };
|
|
8224
8603
|
},
|
|
8225
8604
|
});
|
|
8226
8605
|
```
|
|
@@ -8250,17 +8629,20 @@ warlock seed # run all auto-discovered seeds
|
|
|
8250
8629
|
warlock seed --list # show registry (name, order, enabled), don't run
|
|
8251
8630
|
warlock seed --path=<file> # run one file by absolute or relative path
|
|
8252
8631
|
warlock seed --fresh # truncate every table first, then run all
|
|
8632
|
+
warlock seed --drop # undo every tracked record, reset the seeds-log
|
|
8633
|
+
warlock seed --drop=<name> # undo just one seeder's tracked records
|
|
8253
8634
|
warlock seed --transaction # (default true) — pass --transaction=false to skip wrapping
|
|
8254
8635
|
```
|
|
8255
8636
|
|
|
8256
|
-
`--fresh` is **destructive** — it truncates all tables, including the `seeds` tracking table. Treat it as "reset the dev DB," not "run pending seeds."
|
|
8637
|
+
`--fresh` is **destructive** — it truncates all tables, including the `seeds` tracking table. Treat it as "reset the dev DB," not "run pending seeds." `--drop` is the surgical undo — only tracked rows, scoped to one seeder if you name it.
|
|
8257
8638
|
|
|
8258
8639
|
## Gotchas
|
|
8259
8640
|
|
|
8260
8641
|
- **The seed file must default-export.** A named export gets the discovery error at boot. Wrap with `export default seeder({...})`.
|
|
8261
8642
|
- **`once: true` only works if the seed run succeeds.** A throw rolls back the transaction including the `seeds` table insert — the next run will retry.
|
|
8262
|
-
-
|
|
8263
|
-
- **`
|
|
8643
|
+
- **`--drop` only undoes what you `track()`.** A seed that never calls `track()` has nothing in `seed_records`, so `--drop` can't reverse it. Track every row you want to be able to undo.
|
|
8644
|
+
- **`dependsOn` is resolved** (topological sort over `order`). An unknown dependency throws `UnknownSeederDependencyError`; a cycle throws `SeederDependencyCycleError`. A `dependsOn` pointing at a `enabled: false` seeder errors, because disabled seeds are filtered out before resolution.
|
|
8645
|
+
- **`batchSize` is surfaced into the run context, not acted on by the manager.** Set `batchSize` on the seeder and the manager hands the value through as `run({ batchSize })` (defaulting to `0`); forward it to `Model.createMany(rows, { batchSize })` to get chunked inserts. The manager itself never chunks — it only relays the value.
|
|
8264
8646
|
- **`--fresh` truncates `cascade: true`.** Foreign-key chains delete with their parents. If you have data outside the seeded scope that you want to keep, do NOT run `--fresh`.
|
|
8265
8647
|
- **Seeds aren't migrations.** Schema changes go in `migrations/`. Seeds populate data into a schema that already exists. Mixing the two (creating a table in a seed) confuses the lifecycle.
|
|
8266
8648
|
- **`enabled: false` skips the seed but it's still in the registry.** `warlock seed --list` shows it as disabled. Re-enable by flipping the flag, not by deleting the file.
|