@warlock.js/cascade 5.27.0 → 5.29.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 +13 -0
- package/cjs/index.cjs +134 -4
- package/cjs/index.cjs.map +1 -1
- package/esm/database-dirty-tracker.d.mts +38 -0
- package/esm/database-dirty-tracker.d.mts.map +1 -1
- package/esm/database-dirty-tracker.mjs +77 -2
- package/esm/database-dirty-tracker.mjs.map +1 -1
- package/esm/model/methods/accessor-methods.mjs +11 -0
- package/esm/model/methods/accessor-methods.mjs.map +1 -1
- package/esm/utils/prune-empty-parents.mjs +38 -0
- package/esm/utils/prune-empty-parents.mjs.map +1 -0
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs +14 -2
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +93 -93
- package/llms.txt +15 -15
- package/package.json +5 -5
- package/skills/README.md +17 -17
- package/skills/aggregate-data/SKILL.md +3 -3
- package/skills/alter-migration/SKILL.md +4 -4
- package/skills/cascade-basics/SKILL.md +21 -21
- package/skills/configure-delete-strategy/SKILL.md +6 -6
- package/skills/define-model/SKILL.md +8 -8
- package/skills/define-relations/SKILL.md +3 -3
- package/skills/manage-data-sources/SKILL.md +4 -4
- package/skills/manage-transactions/SKILL.md +4 -4
- package/skills/paginate-results/SKILL.md +4 -4
- package/skills/perform-atomic-ops/SKILL.md +5 -5
- package/skills/query-data/SKILL.md +10 -10
- package/skills/run-cascade-cli/SKILL.md +3 -3
- package/skills/search-by-vector/SKILL.md +3 -3
- package/skills/subscribe-to-model-events/SKILL.md +5 -5
- package/skills/track-changes/SKILL.md +3 -3
- package/skills/write-migration/SKILL.md +7 -7
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: aggregate-data
|
|
11
|
-
description: 'Compute aggregates over a query — scalar `.count()` / `.sum(field)` / `.avg` / `.min` / `.max`, plus grouped rollups via the two-arg `.groupBy(fields, { alias: $agg.* })`, portable date-bucketing via `.groupByDate(col, unit, aggregates?)`, the `$agg` helpers (including expression-aware `$agg.sum($expr.mul("price","quantity"))` / `$agg.sumRaw`), and `.having(alias, op, value)` on computed aggregates, plus MongoDB pipeline stages `.unwind(field, options?)` / `.addFields(fields)` / pipeline-form `join()` (`UnsupportedQueryOperationError` on Postgres). Triggers: `.unwind`, `.addFields`, `.joinRaw`, `.raw`, `$unwind`, `$lookup`, `.count`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.groupByDate`, `.having`, `$agg`, `$agg.sum`, `$agg.sumRaw`, `$agg.count`, `$expr`, `$expr.mul`, `$expr.col`, `$expr.lit`; "monthly revenue report", "revenue per month", "X per category", "group by status", "sum price times quantity", "dashboard rollup"; typical import `import { Model, $agg, $expr } from "@warlock.js/cascade"`. Skip: row queries —
|
|
11
|
+
description: 'Compute aggregates over a query — scalar `.count()` / `.sum(field)` / `.avg` / `.min` / `.max`, plus grouped rollups via the two-arg `.groupBy(fields, { alias: $agg.* })`, portable date-bucketing via `.groupByDate(col, unit, aggregates?)`, the `$agg` helpers (including expression-aware `$agg.sum($expr.mul("price","quantity"))` / `$agg.sumRaw`), and `.having(alias, op, value)` on computed aggregates, plus MongoDB pipeline stages `.unwind(field, options?)` / `.addFields(fields)` / pipeline-form `join()` (`UnsupportedQueryOperationError` on Postgres). Triggers: `.unwind`, `.addFields`, `.joinRaw`, `.raw`, `$unwind`, `$lookup`, `.count`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.groupByDate`, `.having`, `$agg`, `$agg.sum`, `$agg.sumRaw`, `$agg.count`, `$expr`, `$expr.mul`, `$expr.col`, `$expr.lit`; "monthly revenue report", "revenue per month", "X per category", "group by status", "sum price times quantity", "dashboard rollup"; typical import `import { Model, $agg, $expr } from "@warlock.js/cascade"`. Skip: row queries — the `query-data` topic; cached aggregates — the `use-cached-hof` topic of the `warlock-js-cache` skill; competing tools raw SQL `GROUP BY`, `mongoose aggregate`, `prisma` `groupBy`.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Use aggregates and groupBy
|
|
@@ -169,15 +169,15 @@ Raw stages: `joinRaw({ $lookup: { from, let, pipeline, as } })` (or an array of
|
|
|
169
169
|
|
|
170
170
|
## See also
|
|
171
171
|
|
|
172
|
-
-
|
|
173
|
-
-
|
|
172
|
+
- The `query-data` topic — the broader query vocabulary
|
|
173
|
+
- The `use-cached-hof` topic of the `warlock-js-cache` skill — caching expensive aggregate results
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
## alter-migration `@warlock.js/cascade/alter-migration/SKILL.md`
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
name: alter-migration
|
|
180
|
-
description: 'Evolve an existing table with `Migration.alter(Model, schema, options?)` — add/drop/rename/modify columns; add/drop regular, unique, expression, full-text, geo, vector, and TTL indexes; add/drop foreign keys and CHECK constraints; write rollbacks with class-form methods in `down()`. Triggers: "alter a table", "add a column to existing table", "drop a column", "rename a column", "add an index", "drop a unique constraint", "change a column type", `Migration.alter`, `dropUnique`, `addIndex`, `addForeign`. Skip: creating a brand-new table —
|
|
180
|
+
description: 'Evolve an existing table with `Migration.alter(Model, schema, options?)` — add/drop/rename/modify columns; add/drop regular, unique, expression, full-text, geo, vector, and TTL indexes; add/drop foreign keys and CHECK constraints; write rollbacks with class-form methods in `down()`. Triggers: "alter a table", "add a column to existing table", "drop a column", "rename a column", "add an index", "drop a unique constraint", "change a column type", `Migration.alter`, `dropUnique`, `addIndex`, `addForeign`. Skip: creating a brand-new table — the `write-migration` topic.'
|
|
181
181
|
---
|
|
182
182
|
|
|
183
183
|
# Alter a migration
|
|
@@ -703,9 +703,9 @@ export default Migration.alter(Product, {
|
|
|
703
703
|
|
|
704
704
|
## See also
|
|
705
705
|
|
|
706
|
-
-
|
|
707
|
-
-
|
|
708
|
-
-
|
|
706
|
+
- The `write-migration` topic — `Migration.create` for brand-new tables + all column helpers
|
|
707
|
+
- The `run-cascade-cli` topic — CLI flags + programmatic Operations API
|
|
708
|
+
- The `manage-data-sources` topic — multi-DB migrations
|
|
709
709
|
|
|
710
710
|
|
|
711
711
|
## cascade-basics `@warlock.js/cascade/cascade-basics/SKILL.md`
|
|
@@ -736,15 +736,15 @@ npm install @warlock.js/cascade @warlock.js/seal
|
|
|
736
736
|
The 10 things that are true in every cascade use:
|
|
737
737
|
|
|
738
738
|
1. **The model is the query entry point.** `User.where(...)`, `User.find(id)`, `User.paginate(...)`. No `db.users`, no separate client, no repository layer.
|
|
739
|
-
2. **One schema does triple duty.** `userSchema` via `v.object({...})` is your TS type (`Infer<typeof userSchema>`), your runtime validator on save, and the shape your migration writes against. Defined via
|
|
739
|
+
2. **One schema does triple duty.** `userSchema` via `v.object({...})` is your TS type (`Infer<typeof userSchema>`), your runtime validator on save, and the shape your migration writes against. Defined via the `seal-basics` topic of the `warlock-js-seal` skill.
|
|
740
740
|
3. **`@RegisterModel()` puts the model in the global registry.** Other models look it up by name for relations (`@BelongsTo("User")` or `@BelongsTo(lazy(() => User))`).
|
|
741
741
|
4. **Two drivers ship in-box: MongoDB and Postgres.** Same query API across both. Switch via config; the call sites stay identical.
|
|
742
|
-
5. **Migrations are required.** `cascade migrate` runs schema changes; the model class doesn't auto-create tables. See
|
|
743
|
-
6. **`.create()` validates against the schema before persisting.** Defaults (`v.string().default(...)`) fire here. Validation errors throw — see
|
|
744
|
-
7. **Three update idioms — pick by shape.** `.set(k, v).save()` for 1–2 fields, `.merge(data).save()` for object payloads, `.save()` after spread mutations. See
|
|
745
|
-
8. **`.destroy()` runs the configured delete strategy** (`permanent` / `soft` / `trash`). See
|
|
746
|
-
9. **Lifecycle events fire on every meaningful moment** — `saving` / `saved`, `creating` / `created`, `deleting` / `deleted`. Hook on the model class. See
|
|
747
|
-
10. **Transactions are first-class.** `transaction(async () => { ... })` wraps a unit; rollback on throw, commit on resolve. See
|
|
742
|
+
5. **Migrations are required.** `cascade migrate` runs schema changes; the model class doesn't auto-create tables. See the `write-migration` topic.
|
|
743
|
+
6. **`.create()` validates against the schema before persisting.** Defaults (`v.string().default(...)`) fire here. Validation errors throw — see the `handle-seal-errors` topic of the `warlock-js-seal` skill.
|
|
744
|
+
7. **Three update idioms — pick by shape.** `.set(k, v).save()` for 1–2 fields, `.merge(data).save()` for object payloads, `.save()` after spread mutations. See the `define-model` topic.
|
|
745
|
+
8. **`.destroy()` runs the configured delete strategy** (`permanent` / `soft` / `trash`). See the `configure-delete-strategy` topic.
|
|
746
|
+
9. **Lifecycle events fire on every meaningful moment** — `saving` / `saved`, `creating` / `created`, `deleting` / `deleted`. Hook on the model class. See the `subscribe-to-model-events` topic.
|
|
747
|
+
10. **Transactions are first-class.** `transaction(async () => { ... })` wraps a unit; rollback on throw, commit on resolve. See the `manage-transactions` topic.
|
|
748
748
|
|
|
749
749
|
## Minimal example — model, write, read
|
|
750
750
|
|
|
@@ -784,20 +784,20 @@ const page = await User.paginate({ page: 1, limit: 20 });
|
|
|
784
784
|
|
|
785
785
|
| If the task is about… | Load |
|
|
786
786
|
| --- | --- |
|
|
787
|
-
| Defining a model class — schema, decorators, `Model<TSchema>`, accessors |
|
|
788
|
-
| Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering |
|
|
789
|
-
| Pagination — `.paginate`, `cursorPaginate`, `chunk` |
|
|
790
|
-
| Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading |
|
|
791
|
-
| Migrations — `migration` definition, `up`/`down`, CLI |
|
|
792
|
-
| Transactions — `transaction(fn)`, rollback, isolation |
|
|
793
|
-
| Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` |
|
|
794
|
-
| Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` |
|
|
795
|
-
| Soft / hard / trash deletes + restore |
|
|
796
|
-
| Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` |
|
|
797
|
-
| Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` |
|
|
798
|
-
| Vector search — `similarTo`, pgvector / Atlas vector index |
|
|
799
|
-
| Multiple databases — `connectToDatabase`, per-model `static dataSource` |
|
|
800
|
-
| CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic |
|
|
787
|
+
| Defining a model class — schema, decorators, `Model<TSchema>`, accessors | The `define-model` topic |
|
|
788
|
+
| Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering | The `query-data` topic |
|
|
789
|
+
| Pagination — `.paginate`, `cursorPaginate`, `chunk` | The `paginate-results` topic |
|
|
790
|
+
| Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading | The `define-relations` topic |
|
|
791
|
+
| Migrations — `migration` definition, `up`/`down`, CLI | The `write-migration` topic |
|
|
792
|
+
| Transactions — `transaction(fn)`, rollback, isolation | The `manage-transactions` topic |
|
|
793
|
+
| Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` | The `track-changes` topic |
|
|
794
|
+
| Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` | The `subscribe-to-model-events` topic |
|
|
795
|
+
| Soft / hard / trash deletes + restore | The `configure-delete-strategy` topic |
|
|
796
|
+
| Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` | The `perform-atomic-ops` topic |
|
|
797
|
+
| Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` | The `aggregate-data` topic |
|
|
798
|
+
| Vector search — `similarTo`, pgvector / Atlas vector index | The `search-by-vector` topic |
|
|
799
|
+
| Multiple databases — `connectToDatabase`, per-model `static dataSource` | The `manage-data-sources` topic |
|
|
800
|
+
| CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic | The `run-cascade-cli` topic |
|
|
801
801
|
|
|
802
802
|
## Things NOT to do
|
|
803
803
|
|
|
@@ -813,7 +813,7 @@ const page = await User.paginate({ page: 1, limit: 20 });
|
|
|
813
813
|
|
|
814
814
|
---
|
|
815
815
|
name: configure-delete-strategy
|
|
816
|
-
description: 'Pick the delete behavior — `permanent` (hard delete), `soft` (set `deletedAt`, keep the row), `trash` (move to a separate table). Configure via `static deleteStrategy` or `.destroy({ strategy })`; restore via static `Model.restore(id)` / `Model.restoreAll()`. Triggers: `static deleteStrategy`, `.destroy`, `Model.restore`, `Model.restoreAll`, `deletedAtColumn`, `trashTable`; "soft delete users", "restore a deleted record", "GDPR hard delete"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: lifecycle events —
|
|
816
|
+
description: 'Pick the delete behavior — `permanent` (hard delete), `soft` (set `deletedAt`, keep the row), `trash` (move to a separate table). Configure via `static deleteStrategy` or `.destroy({ strategy })`; restore via static `Model.restore(id)` / `Model.restoreAll()`. Triggers: `static deleteStrategy`, `.destroy`, `Model.restore`, `Model.restoreAll`, `deletedAtColumn`, `trashTable`; "soft delete users", "restore a deleted record", "GDPR hard delete"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: lifecycle events — the `subscribe-to-model-events` topic; competing libs `mongoose-delete`, `typeorm softRemove`, `sequelize` paranoid.'
|
|
817
817
|
---
|
|
818
818
|
|
|
819
819
|
# Use delete strategies
|
|
@@ -844,7 +844,7 @@ export class User extends Model<UserSchema> {
|
|
|
844
844
|
}
|
|
845
845
|
```
|
|
846
846
|
|
|
847
|
-
When the strategy resolves to `"soft"`, `Migration.create(Model, { … })` adds the `deletedAt` column for you (using `deletedAtColumn`), so the schema matches what `destroy()` writes — no need to declare it in the migration. Opt out per table with `{ softDeletes: false }`. See
|
|
847
|
+
When the strategy resolves to `"soft"`, `Migration.create(Model, { … })` adds the `deletedAt` column for you (using `deletedAtColumn`), so the schema matches what `destroy()` writes — no need to declare it in the migration. Opt out per table with `{ softDeletes: false }`. See the `write-migration` topic.
|
|
848
848
|
|
|
849
849
|
## Override per-call
|
|
850
850
|
|
|
@@ -909,7 +909,7 @@ await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-dele
|
|
|
909
909
|
await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
|
|
910
910
|
```
|
|
911
911
|
|
|
912
|
-
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See
|
|
912
|
+
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See the `query-data` topic for scopes.
|
|
913
913
|
|
|
914
914
|
## Lifecycle hooks fire on all strategies
|
|
915
915
|
|
|
@@ -927,7 +927,7 @@ User.on("restored", async (user) => {
|
|
|
927
927
|
});
|
|
928
928
|
```
|
|
929
929
|
|
|
930
|
-
See
|
|
930
|
+
See the `subscribe-to-model-events` topic.
|
|
931
931
|
|
|
932
932
|
## Things NOT to do
|
|
933
933
|
|
|
@@ -939,15 +939,15 @@ See [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/casca
|
|
|
939
939
|
|
|
940
940
|
## See also
|
|
941
941
|
|
|
942
|
-
-
|
|
943
|
-
-
|
|
942
|
+
- The `subscribe-to-model-events` topic — `deleting` / `deleted` / `restored` events
|
|
943
|
+
- The `query-data` topic — global scopes and `withoutGlobalScope` for surfacing deleted rows
|
|
944
944
|
|
|
945
945
|
|
|
946
946
|
## define-model `@warlock.js/cascade/define-model/SKILL.md`
|
|
947
947
|
|
|
948
948
|
---
|
|
949
949
|
name: define-model
|
|
950
|
-
description: 'Define a Cascade model — `@RegisterModel()`, class extends `Model<TSchema>`, `static table`, `static schema`, three update idioms (`.set` / `.merge` / `.save`), `.unset`, `.destroy`, `static toJsonColumns` / `resource` / `static hidden` for output shaping. Covers write safety: `static hidden` fields are ALWAYS stripped from `toJSON()` regardless of `resource`/`toJsonColumns`, cascade warns once per model on an unhidden credential-shaped field, and `merge()` on a persisted model drops identity columns so request data can never retarget an `update()`/`destroy()` at another row. Triggers: `Model`, `RegisterModel`, `static schema`, `.set`, `.merge`, `.save`, `.unset`, `.destroy`, `toJsonColumns`, `resource`, `hidden`, `trustedPrimaryKey`; "how do I define a model", "shape the JSON output", "remove a field", "hide password from response", "prevent id in request body overwriting another row"; typical import `import { Model, RegisterModel } from "@warlock.js/cascade"`. Skip: querying —
|
|
950
|
+
description: 'Define a Cascade model — `@RegisterModel()`, class extends `Model<TSchema>`, `static table`, `static schema`, three update idioms (`.set` / `.merge` / `.save`), `.unset`, `.destroy`, `static toJsonColumns` / `resource` / `static hidden` for output shaping. Covers write safety: `static hidden` fields are ALWAYS stripped from `toJSON()` regardless of `resource`/`toJsonColumns`, cascade warns once per model on an unhidden credential-shaped field, and `merge()` on a persisted model drops identity columns so request data can never retarget an `update()`/`destroy()` at another row. Triggers: `Model`, `RegisterModel`, `static schema`, `.set`, `.merge`, `.save`, `.unset`, `.destroy`, `toJsonColumns`, `resource`, `hidden`, `trustedPrimaryKey`; "how do I define a model", "shape the JSON output", "remove a field", "hide password from response", "prevent id in request body overwriting another row"; typical import `import { Model, RegisterModel } from "@warlock.js/cascade"`. Skip: querying — the `query-data` topic; relations — the `define-relations` topic; competing libs `mongoose`, `prisma`, `typeorm` `@Entity`.'
|
|
951
951
|
---
|
|
952
952
|
|
|
953
953
|
# Define a model
|
|
@@ -973,7 +973,7 @@ export type UserSchema = Infer<typeof userSchema>;
|
|
|
973
973
|
- Fields are **required by default** — chain `.optional()` on any field that may be missing.
|
|
974
974
|
- `Infer<typeof userSchema>` derives the TS type. No second declaration, no drift.
|
|
975
975
|
- The schema is standalone — reuse it for HTTP body validation, service-input validation, anywhere else.
|
|
976
|
-
- See
|
|
976
|
+
- See the `seal-basics` topic of the `warlock-js-seal` skill for the validator vocabulary.
|
|
977
977
|
|
|
978
978
|
## Step 2 — Define the model class
|
|
979
979
|
|
|
@@ -1041,7 +1041,7 @@ await user.merge({ name: "Augusta Ada King", status: "active" }).save();
|
|
|
1041
1041
|
|
|
1042
1042
|
The everyday case. Service takes `Partial<UserSchema>` from a request body, merges it into the instance, saves. Existing fields not in the object are untouched.
|
|
1043
1043
|
|
|
1044
|
-
**Safe against id-retargeting by design.** `model.merge(req.body); await model.save();` is the canonical update-my-profile shape — and `req.body` is attacker-controlled. On an already-persisted model, `merge()` drops identity columns (`id`, `_id`, the configured primary key) instead of applying them, so a body carrying `{ id: "<victim-id>", role: "admin" }` cannot redirect the write onto someone else's row: `update()`/`replace()`/`destroy()` build their filter from `model.trustedPrimaryKey` — the id captured when the instance became persisted (hydration, or right after an insert), never the current in-memory value. Identity columns are also excluded from the `$set`/`$unset` payload, so an explicit `.set("id", …)` on a loaded record doesn't rewrite the key of the row it's pinned to either. This only applies to an *existing* record — `User.create({ id, ... })` with an explicit id is unchanged, since there's no row to retarget yet. If you deliberately need to change a primary key, do it through the atomic/raw APIs (see
|
|
1044
|
+
**Safe against id-retargeting by design.** `model.merge(req.body); await model.save();` is the canonical update-my-profile shape — and `req.body` is attacker-controlled. On an already-persisted model, `merge()` drops identity columns (`id`, `_id`, the configured primary key) instead of applying them, so a body carrying `{ id: "<victim-id>", role: "admin" }` cannot redirect the write onto someone else's row: `update()`/`replace()`/`destroy()` build their filter from `model.trustedPrimaryKey` — the id captured when the instance became persisted (hydration, or right after an insert), never the current in-memory value. Identity columns are also excluded from the `$set`/`$unset` payload, so an explicit `.set("id", …)` on a loaded record doesn't rewrite the key of the row it's pinned to either. This only applies to an *existing* record — `User.create({ id, ... })` with an explicit id is unchanged, since there's no row to retarget yet. If you deliberately need to change a primary key, do it through the atomic/raw APIs (see the `perform-atomic-ops` topic), not `merge()`.
|
|
1045
1045
|
|
|
1046
1046
|
### `.save()` after manual mutation — when changes are spread
|
|
1047
1047
|
|
|
@@ -1077,7 +1077,7 @@ await user.unset("image").save();
|
|
|
1077
1077
|
await user.destroy();
|
|
1078
1078
|
```
|
|
1079
1079
|
|
|
1080
|
-
Runs the model's lifecycle (events, configured delete strategy), then removes the record. See
|
|
1080
|
+
Runs the model's lifecycle (events, configured delete strategy), then removes the record. See the `configure-delete-strategy` topic for soft / hard / trash semantics.
|
|
1081
1081
|
|
|
1082
1082
|
## Public output shaping
|
|
1083
1083
|
|
|
@@ -1146,17 +1146,17 @@ export class User extends Model<UserSchema> {
|
|
|
1146
1146
|
|
|
1147
1147
|
## See also
|
|
1148
1148
|
|
|
1149
|
-
-
|
|
1150
|
-
-
|
|
1151
|
-
-
|
|
1152
|
-
-
|
|
1149
|
+
- The `query-data` topic — finding and filtering records
|
|
1150
|
+
- The `define-relations` topic — relations and eager loading
|
|
1151
|
+
- The `track-changes` topic — dirty tracking
|
|
1152
|
+
- The `subscribe-to-model-events` topic — lifecycle hooks
|
|
1153
1153
|
|
|
1154
1154
|
|
|
1155
1155
|
## define-relations `@warlock.js/cascade/define-relations/SKILL.md`
|
|
1156
1156
|
|
|
1157
1157
|
---
|
|
1158
1158
|
name: define-relations
|
|
1159
|
-
description: 'Define and query relations — `@BelongsTo` / `@HasMany` / `@BelongsToMany`, `.with("relation")` eager loading, `.whereHas(relation, cb)` filter-by-related, `setRelation` on save, `.joinWith` for SQL joins, `loadRelation`, `lazy(() => Model)`. Triggers: `@BelongsTo`, `@HasMany`, `@BelongsToMany`, `.with`, `.whereHas`, `setRelation`, `.joinWith`, `lazy`; "define a relation", "avoid N+1", "eager load posts", "filter parents by child"; typical import `import { BelongsTo, HasMany, BelongsToMany } from "@warlock.js/cascade"`. Skip: model basics —
|
|
1159
|
+
description: 'Define and query relations — `@BelongsTo` / `@HasMany` / `@BelongsToMany`, `.with("relation")` eager loading, `.whereHas(relation, cb)` filter-by-related, `setRelation` on save, `.joinWith` for SQL joins, `loadRelation`, `lazy(() => Model)`. Triggers: `@BelongsTo`, `@HasMany`, `@BelongsToMany`, `.with`, `.whereHas`, `setRelation`, `.joinWith`, `lazy`; "define a relation", "avoid N+1", "eager load posts", "filter parents by child"; typical import `import { BelongsTo, HasMany, BelongsToMany } from "@warlock.js/cascade"`. Skip: model basics — the `define-model` topic; competing libs `mongoose populate`, `prisma include`, `typeorm relations`.'
|
|
1160
1160
|
---
|
|
1161
1161
|
|
|
1162
1162
|
# Define and query relations
|
|
@@ -1323,15 +1323,15 @@ For embedded/denormalized relations (cached `author_name` on posts, etc.), Casca
|
|
|
1323
1323
|
|
|
1324
1324
|
## See also
|
|
1325
1325
|
|
|
1326
|
-
-
|
|
1327
|
-
-
|
|
1326
|
+
- The `query-data` topic — `.whereHas` and other filters
|
|
1327
|
+
- The `define-model` topic — `@RegisterModel`, the registry that makes name-based lookup work
|
|
1328
1328
|
|
|
1329
1329
|
|
|
1330
1330
|
## manage-data-sources `@warlock.js/cascade/manage-data-sources/SKILL.md`
|
|
1331
1331
|
|
|
1332
1332
|
---
|
|
1333
1333
|
name: manage-data-sources
|
|
1334
|
-
description: 'Configure multiple databases — register each via `connectToDatabase({ name, driver, database, isDefault })`, assign a model with `static dataSource = "name"`, route a migration with `dataSource` on the migration class, inspect via `dataSourceRegistry.get(name)` / `getAllDataSources()`. The first (or `isDefault: true`) source is the default. Triggers: `connectToDatabase`, `dataSourceRegistry`, `dataSourceRegistry.get`, `getAllDataSources`, `static dataSource`; "multi-database app", "per-tenant DB", "analytics on separate DB"; typical import `import { connectToDatabase, dataSourceRegistry } from "@warlock.js/cascade"`. Skip: per-source migrations —
|
|
1334
|
+
description: 'Configure multiple databases — register each via `connectToDatabase({ name, driver, database, isDefault })`, assign a model with `static dataSource = "name"`, route a migration with `dataSource` on the migration class, inspect via `dataSourceRegistry.get(name)` / `getAllDataSources()`. The first (or `isDefault: true`) source is the default. Triggers: `connectToDatabase`, `dataSourceRegistry`, `dataSourceRegistry.get`, `getAllDataSources`, `static dataSource`; "multi-database app", "per-tenant DB", "analytics on separate DB"; typical import `import { connectToDatabase, dataSourceRegistry } from "@warlock.js/cascade"`. Skip: per-source migrations — the `write-migration` topic; transaction scope — the `manage-transactions` topic; competing patterns `mongoose.createConnection`, `typeorm` `DataSource`, `prisma` multi-schema.'
|
|
1335
1335
|
---
|
|
1336
1336
|
|
|
1337
1337
|
# Manage data sources
|
|
@@ -1417,7 +1417,7 @@ export default class CreateEventsTable extends Migration {
|
|
|
1417
1417
|
}
|
|
1418
1418
|
```
|
|
1419
1419
|
|
|
1420
|
-
See
|
|
1420
|
+
See the `write-migration` topic for the migration shape.
|
|
1421
1421
|
|
|
1422
1422
|
## Transactions and data sources
|
|
1423
1423
|
|
|
@@ -1504,15 +1504,15 @@ unless `clientOptions.monitorCommands` is explicitly set.
|
|
|
1504
1504
|
|
|
1505
1505
|
## See also
|
|
1506
1506
|
|
|
1507
|
-
-
|
|
1508
|
-
-
|
|
1507
|
+
- The `write-migration` topic — per-source migrations
|
|
1508
|
+
- The `manage-transactions` topic — transactions run on the default source
|
|
1509
1509
|
|
|
1510
1510
|
|
|
1511
1511
|
## manage-transactions `@warlock.js/cascade/manage-transactions/SKILL.md`
|
|
1512
1512
|
|
|
1513
1513
|
---
|
|
1514
1514
|
name: manage-transactions
|
|
1515
|
-
description: 'Wrap multi-statement work in `transaction(async () => {...})` — rollback on throw, commit on resolve, optional `isolation` level (Postgres), per-`dataSource` scope. Also the home for row locking (`lockForUpdate({ skipLocked })` → `SELECT ... FOR UPDATE [SKIP LOCKED | NOWAIT]`, Postgres-only), transaction-aware raw SQL (`Model.raw` / `DataSource.raw` → `RawQueryResult`) and Postgres native-array column handling (`JSONB[]` / `TEXT[]` / `INTEGER[]` auto-detected via schema introspection on connect; `nativeArrayColumns` is an optional override). Postgres native; MongoDB requires replica set. Triggers: `transaction`, `isolation`, `SERIALIZABLE`, `READ COMMITTED`, nested transaction, flat nesting, nested savepoints, `lockForUpdate`, `skipLocked`, `FOR UPDATE`, `SKIP LOCKED`, `NOWAIT`, row lock, pessimistic lock, queue claim, `Model.raw`, `DataSource.raw`, raw SQL, `RawQueryResult`, `nativeArrayColumns`, `JSONB[]`, `TEXT[]`; "wrap two writes atomically", "transfer balance between accounts", "rollback on error", "MongoDB replica set transactions", "lock rows so workers don''t double-process", "claim jobs from a table", "run raw SQL", "native array column", "malformed array literal", "array column not saving", "nested transaction not visible", "foreign key violation on insert inside transaction", "service transaction inside seeder"; typical import `import { transaction } from "@warlock.js/cascade"`. Skip: single-row atomic ops without a transaction —
|
|
1515
|
+
description: 'Wrap multi-statement work in `transaction(async () => {...})` — rollback on throw, commit on resolve, optional `isolation` level (Postgres), per-`dataSource` scope. Also the home for row locking (`lockForUpdate({ skipLocked })` → `SELECT ... FOR UPDATE [SKIP LOCKED | NOWAIT]`, Postgres-only), transaction-aware raw SQL (`Model.raw` / `DataSource.raw` → `RawQueryResult`) and Postgres native-array column handling (`JSONB[]` / `TEXT[]` / `INTEGER[]` auto-detected via schema introspection on connect; `nativeArrayColumns` is an optional override). Postgres native; MongoDB requires replica set. Triggers: `transaction`, `isolation`, `SERIALIZABLE`, `READ COMMITTED`, nested transaction, flat nesting, nested savepoints, `lockForUpdate`, `skipLocked`, `FOR UPDATE`, `SKIP LOCKED`, `NOWAIT`, row lock, pessimistic lock, queue claim, `Model.raw`, `DataSource.raw`, raw SQL, `RawQueryResult`, `nativeArrayColumns`, `JSONB[]`, `TEXT[]`; "wrap two writes atomically", "transfer balance between accounts", "rollback on error", "MongoDB replica set transactions", "lock rows so workers don''t double-process", "claim jobs from a table", "run raw SQL", "native array column", "malformed array literal", "array column not saving", "nested transaction not visible", "foreign key violation on insert inside transaction", "service transaction inside seeder"; typical import `import { transaction } from "@warlock.js/cascade"`. Skip: single-row atomic ops without a transaction — the `perform-atomic-ops` topic; per-source scope — the `manage-data-sources` topic; competing patterns `mongoose.startSession`, `pg` `BEGIN` manually, `prisma.$transaction`, `typeorm` `QueryRunner`.'
|
|
1516
1516
|
---
|
|
1517
1517
|
|
|
1518
1518
|
# Use transactions
|
|
@@ -1664,7 +1664,7 @@ const claimed = await Job.atomic(
|
|
|
1664
1664
|
);
|
|
1665
1665
|
```
|
|
1666
1666
|
|
|
1667
|
-
See
|
|
1667
|
+
See the `perform-atomic-ops` topic for `atomic` / `findOneAndUpdate`.
|
|
1668
1668
|
|
|
1669
1669
|
## Outside the transaction
|
|
1670
1670
|
|
|
@@ -1741,15 +1741,15 @@ Use the outbox pattern above instead when the side effect must survive a crash b
|
|
|
1741
1741
|
|
|
1742
1742
|
## See also
|
|
1743
1743
|
|
|
1744
|
-
-
|
|
1745
|
-
-
|
|
1744
|
+
- The `perform-atomic-ops` topic — atomic single-document ops without a full transaction
|
|
1745
|
+
- The `manage-data-sources` topic — transactions run on the default source
|
|
1746
1746
|
|
|
1747
1747
|
|
|
1748
1748
|
## paginate-results `@warlock.js/cascade/paginate-results/SKILL.md`
|
|
1749
1749
|
|
|
1750
1750
|
---
|
|
1751
1751
|
name: paginate-results
|
|
1752
|
-
description: 'Paginate query results — `.paginate({page, limit, filter?})` for offset (returns `data` + `pagination` total/page/limit/pages), `.cursorPaginate({limit, cursor})` for very large datasets, `.chunk(size, callback)` for streaming. Triggers: `.paginate`, `.cursorPaginate`, `.chunk`, `nextCursor`, `hasMore`, `pagination.total`; "paginate the list", "infinite scroll / load more", "stream a large table", "page 2 of users"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: filter chain —
|
|
1752
|
+
description: 'Paginate query results — `.paginate({page, limit, filter?})` for offset (returns `data` + `pagination` total/page/limit/pages), `.cursorPaginate({limit, cursor})` for very large datasets, `.chunk(size, callback)` for streaming. Triggers: `.paginate`, `.cursorPaginate`, `.chunk`, `nextCursor`, `hasMore`, `pagination.total`; "paginate the list", "infinite scroll / load more", "stream a large table", "page 2 of users"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: filter chain — the `query-data` topic; eager loading on pages — the `define-relations` topic; competing libs `mongoose-paginate-v2`, `prisma` cursor, `typeorm-pagination`.'
|
|
1753
1753
|
---
|
|
1754
1754
|
|
|
1755
1755
|
# Paginate results
|
|
@@ -1838,7 +1838,7 @@ Eager-load relations on a paginated page:
|
|
|
1838
1838
|
const page = await Post.with("author").paginate({ page: 1, limit: 20 });
|
|
1839
1839
|
```
|
|
1840
1840
|
|
|
1841
|
-
See
|
|
1841
|
+
See the `define-relations` topic.
|
|
1842
1842
|
|
|
1843
1843
|
## Pagination shape
|
|
1844
1844
|
|
|
@@ -1867,15 +1867,15 @@ The total count requires an extra query. On very large filtered tables, this can
|
|
|
1867
1867
|
|
|
1868
1868
|
## See also
|
|
1869
1869
|
|
|
1870
|
-
-
|
|
1871
|
-
-
|
|
1870
|
+
- The `query-data` topic — `.where`, `.orderBy`, filter chains
|
|
1871
|
+
- The `define-relations` topic — eager loading on a paginated page
|
|
1872
1872
|
|
|
1873
1873
|
|
|
1874
1874
|
## perform-atomic-ops `@warlock.js/cascade/perform-atomic-ops/SKILL.md`
|
|
1875
1875
|
|
|
1876
1876
|
---
|
|
1877
1877
|
name: perform-atomic-ops
|
|
1878
|
-
description: 'Avoid races on concurrent writes — `Model.increase(filter, field, n)` / `Model.decrease` for atomic counters, `Model.atomic(filter, ops, options?)` for arbitrary mutations (`$set` / `$inc` / `$push` / `$pull` / `$addToSet` / `$setOnInsert`, pipeline updates, `upsert`, `returnDocument`, `arrayFilters`, `trustedFilter`), `Model.createMany` / `Model.findAndUpdate` / `Model.delete` for bulk. `atomic()` / `findAndUpdate()` / `findOneAndUpdate()` / `findAndReplace()` / `findOneAndDelete()` sanitize their `filter` argument — `$`-prefixed keys throw `UnsafeFilterError`, same check as `where()`. Triggers: `Model.increase`, `Model.decrease`, `Model.atomic`, `Model.createMany`, `createMany bulk`, `batchSize`, `Model.findAndUpdate`, `Model.findOneAndUpdate`, `Model.findAndReplace`, `Model.findOneAndDelete`, `Model.delete`, `$inc`, `$set`, `UnsafeFilterError`, `UnsupportedUpdateOperationError`, `upsert`, `returnDocument`, `$setOnInsert`; "upsert a counter", "reserve quota atomically", "increment counter under concurrency", "bulk insert without N+1", "fast bulk insert", "insert thousands of rows", "atomic update without loading", "is atomic() safe with a request body filter"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: multi-row atomicity —
|
|
1878
|
+
description: 'Avoid races on concurrent writes — `Model.increase(filter, field, n)` / `Model.decrease` for atomic counters, `Model.atomic(filter, ops, options?)` for arbitrary mutations (`$set` / `$inc` / `$push` / `$pull` / `$addToSet` / `$setOnInsert`, pipeline updates, `upsert`, `returnDocument`, `arrayFilters`, `trustedFilter`), `Model.createMany` / `Model.findAndUpdate` / `Model.delete` for bulk. `atomic()` / `findAndUpdate()` / `findOneAndUpdate()` / `findAndReplace()` / `findOneAndDelete()` sanitize their `filter` argument — `$`-prefixed keys throw `UnsafeFilterError`, same check as `where()`. Triggers: `Model.increase`, `Model.decrease`, `Model.atomic`, `Model.createMany`, `createMany bulk`, `batchSize`, `Model.findAndUpdate`, `Model.findOneAndUpdate`, `Model.findAndReplace`, `Model.findOneAndDelete`, `Model.delete`, `$inc`, `$set`, `UnsafeFilterError`, `UnsupportedUpdateOperationError`, `upsert`, `returnDocument`, `$setOnInsert`; "upsert a counter", "reserve quota atomically", "increment counter under concurrency", "bulk insert without N+1", "fast bulk insert", "insert thousands of rows", "atomic update without loading", "is atomic() safe with a request body filter"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: multi-row atomicity — the `manage-transactions` topic; competing patterns `mongoose findOneAndUpdate`, `pg` `UPDATE ... SET x = x + 1`.'
|
|
1879
1879
|
---
|
|
1880
1880
|
|
|
1881
1881
|
# Use atomic operations
|
|
@@ -1902,7 +1902,7 @@ await User.atomic({ id: userId }, {
|
|
|
1902
1902
|
|
|
1903
1903
|
`Model.atomic(filter, operations)` → `Promise<number>`. Driver-flavored atomic mutation — MongoDB has `$set` / `$inc` / `$push` / `$pull` natively; the Postgres driver supports `$set` / `$unset` / `$inc` / `$dec` (and `$setOnInsert` on upsert) and throws `UnsupportedUpdateOperationError` for the rest. Use when you need to combine multiple field changes atomically without loading the model first.
|
|
1904
1904
|
|
|
1905
|
-
**`filter` is sanitized like `where()`.** `atomic()`, `findAndUpdate()`, `findOneAndUpdate()`, `findAndReplace()` and `findOneAndDelete()` all run their `filter` argument through the same `$`-prefixed-key check as `Model.where()` (see
|
|
1905
|
+
**`filter` is sanitized like `where()`.** `atomic()`, `findAndUpdate()`, `findOneAndUpdate()`, `findAndReplace()` and `findOneAndDelete()` all run their `filter` argument through the same `$`-prefixed-key check as `Model.where()` (see the `query-data` topic) and throw `UnsafeFilterError` on a key like `$ne`. Before this, these five bypassed `where()` entirely, so a filter forwarded straight from a request body — `User.atomic(req.body.filter, { $set: { role: "admin" } })` — could still smuggle an operator like `{ role: { $ne: "admin" } }` through as a live query even though `where()` itself was already guarded. Only the FILTER is checked — the update-operator object (`$set`/`$inc`/`$unset`/…) is untouched, since that's meant to carry `$` keys. If you legitimately need operator conditions in the filter, express them through `Model.query().where(...)`, or pass `{ trustedFilter: true }` for a code-authored filter (see below).
|
|
1906
1906
|
|
|
1907
1907
|
## Upsert, returnDocument, pipelines — the options argument
|
|
1908
1908
|
|
|
@@ -2013,7 +2013,7 @@ for (const user of targets) {
|
|
|
2013
2013
|
| Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
|
|
2014
2014
|
| Update one record by id | `Model.update(id, data)` |
|
|
2015
2015
|
| Delete many rows (raw) | `Model.delete(filter)` |
|
|
2016
|
-
| Multi-row read-modify-write | Wrap in a
|
|
2016
|
+
| Multi-row read-modify-write | Wrap in a transaction (see the `manage-transactions` topic) |
|
|
2017
2017
|
| Need lifecycle events / delete strategy per row | `Model.where(...).get()` + iterate + `.save()` / `.destroy()` |
|
|
2018
2018
|
|
|
2019
2019
|
## Things NOT to do
|
|
@@ -2027,15 +2027,15 @@ for (const user of targets) {
|
|
|
2027
2027
|
|
|
2028
2028
|
## See also
|
|
2029
2029
|
|
|
2030
|
-
-
|
|
2031
|
-
-
|
|
2030
|
+
- The `manage-transactions` topic — multi-row atomicity
|
|
2031
|
+
- The `paginate-results` topic — `.chunk` for bulk-processing iteration
|
|
2032
2032
|
|
|
2033
2033
|
|
|
2034
2034
|
## query-data `@warlock.js/cascade/query-data/SKILL.md`
|
|
2035
2035
|
|
|
2036
2036
|
---
|
|
2037
2037
|
name: query-data
|
|
2038
|
-
description: 'Query records via the model — `.where(field, value)` / `.where(field, op, value)`, `.find(id)` / `.first` / `.all`, `.orderBy`, `.count` / `.exists`, `.lean()` plain-object reads, plus `.whereIn` / `.whereBetween` / `.whereLike` / `.pluck` / `.firstOrFail` / scopes via `addScope`. Covers filter safety: `where()` rejects `$`-prefixed keys (`UnsafeFilterError`), `whereRaw()` string form rejects `$where`/`$function`/`$accumulator` (`UnsafeRawExpressionError`), and `whereLike`/`whereStartsWith`/`whereEndsWith`/`whereSearch` match string arguments literally (pass a `RegExp` for pattern semantics). Triggers: `.where`, `.find`, `.first`, `.firstOrFail`, `.all`, `.get`, `.orderBy`, `.exists`, `.whereIn`, `.whereBetween`, `.whereLike`, `.whereRaw`, `.lean`, `UnsupportedLeanOperationError`, `addScope`, `escapeRegex`, `likePatternToRegexSource`, `UnsafeFilterError`, `UnsafeRawExpressionError`; "filter by status", "find by id", "fetch active users", "check existence", "search box query", "is where() safe from injection"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: pagination —
|
|
2038
|
+
description: 'Query records via the model — `.where(field, value)` / `.where(field, op, value)`, `.find(id)` / `.first` / `.all`, `.orderBy`, `.count` / `.exists`, `.lean()` plain-object reads, plus `.whereIn` / `.whereBetween` / `.whereLike` / `.pluck` / `.firstOrFail` / scopes via `addScope`. Covers filter safety: `where()` rejects `$`-prefixed keys (`UnsafeFilterError`), `whereRaw()` string form rejects `$where`/`$function`/`$accumulator` (`UnsafeRawExpressionError`), and `whereLike`/`whereStartsWith`/`whereEndsWith`/`whereSearch` match string arguments literally (pass a `RegExp` for pattern semantics). Triggers: `.where`, `.find`, `.first`, `.firstOrFail`, `.all`, `.get`, `.orderBy`, `.exists`, `.whereIn`, `.whereBetween`, `.whereLike`, `.whereRaw`, `.lean`, `UnsupportedLeanOperationError`, `addScope`, `escapeRegex`, `likePatternToRegexSource`, `UnsafeFilterError`, `UnsafeRawExpressionError`; "filter by status", "find by id", "fetch active users", "check existence", "search box query", "is where() safe from injection"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: pagination — the `paginate-results` topic; aggregates — the `aggregate-data` topic.'
|
|
2039
2039
|
---
|
|
2040
2040
|
|
|
2041
2041
|
# Query data
|
|
@@ -2083,7 +2083,7 @@ Equivalent to chained equalities. Useful when the filter comes from a dynamic so
|
|
|
2083
2083
|
|
|
2084
2084
|
### Filters are safe to build from request data — but only through this API
|
|
2085
2085
|
|
|
2086
|
-
`where(field, value)`, `where(field, "=", value)` and the object form all run the value through a `$`-prefixed-key check before it reaches the driver, so `User.first({ email: req.body.email, password: req.body.password })` throws `UnsafeFilterError` instead of compiling `{ password: { $ne: null } }` into a live MongoDB operator query (the classic NoSQL auth-bypass). This applies to every filter-accepting model static too — `first`, `findFirst`, `findAll`, `count`, `paginate`, `all`, `deleteMany`/`delete`, `deleteOne`, and the atomic/find-and-modify statics in
|
|
2086
|
+
`where(field, value)`, `where(field, "=", value)` and the object form all run the value through a `$`-prefixed-key check before it reaches the driver, so `User.first({ email: req.body.email, password: req.body.password })` throws `UnsafeFilterError` instead of compiling `{ password: { $ne: null } }` into a live MongoDB operator query (the classic NoSQL auth-bypass). This applies to every filter-accepting model static too — `first`, `findFirst`, `findAll`, `count`, `paginate`, `all`, `deleteMany`/`delete`, `deleteOne`, and the atomic/find-and-modify statics in the `perform-atomic-ops` topic. Explicit operator APIs (`where(field, operator, value)` for operators other than `=`, `whereIn`/`whereNull`/`whereBetween`/…, object-form `whereRaw({ ... })`) are intentionally not routed through this check — they already express an operator on purpose. Dotted paths (`"profile.name"`) and plain nested-object equality values remain valid; only literal `$`-prefixed keys are rejected. `sanitizeFilter` / `sanitizeFilterValue` are exported if you need the same check on a filter you forward to a driver-level API directly.
|
|
2087
2087
|
|
|
2088
2088
|
```ts
|
|
2089
2089
|
await User.first({ email, password }); // throws UnsafeFilterError if password is `{ $ne: null }`
|
|
@@ -2137,7 +2137,7 @@ const newest = await User
|
|
|
2137
2137
|
|
|
2138
2138
|
`.orderBy(field, "asc" | "desc")` sorts. Default direction is `"asc"`. Chain multiple `.orderBy()` for tiebreakers.
|
|
2139
2139
|
|
|
2140
|
-
For pagination see
|
|
2140
|
+
For pagination see the `paginate-results` topic.
|
|
2141
2141
|
|
|
2142
2142
|
## Plain objects — `.lean()`
|
|
2143
2143
|
|
|
@@ -2170,7 +2170,7 @@ const activeUsers = await User.all({ status: "active" });
|
|
|
2170
2170
|
|
|
2171
2171
|
`Model.all(filter?)` is the shortcut for "fetch all records matching a simple equality filter, or every record if no filter."
|
|
2172
2172
|
|
|
2173
|
-
**Caution.** `.all()` with no filter loads the entire table. Use
|
|
2173
|
+
**Caution.** `.all()` with no filter loads the entire table. Use pagination (see the `paginate-results` topic) for tables larger than a few hundred rows.
|
|
2174
2174
|
|
|
2175
2175
|
## The wider query vocabulary
|
|
2176
2176
|
|
|
@@ -2184,11 +2184,11 @@ Cascade's query builder has around 60 methods. Reach for these as the need arise
|
|
|
2184
2184
|
| `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
|
|
2185
2185
|
| `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
|
|
2186
2186
|
| `.whereHas(relation, callback)` | Filter by conditions on a related model |
|
|
2187
|
-
| `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates —
|
|
2187
|
+
| `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — the `aggregate-data` topic |
|
|
2188
2188
|
| `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
|
|
2189
2189
|
| `.chunk(size, callback)` | Stream a large table in batches |
|
|
2190
|
-
| `.cursorPaginate({ limit, cursor })` | Cursor pagination —
|
|
2191
|
-
| `.similarTo(column, embedding)` | Vector similarity —
|
|
2190
|
+
| `.cursorPaginate({ limit, cursor })` | Cursor pagination — the `paginate-results` topic |
|
|
2191
|
+
| `.similarTo(column, embedding)` | Vector similarity — the `search-by-vector` topic |
|
|
2192
2192
|
|
|
2193
2193
|
Each chains off `User.where(...)` or `User.query()` and ends with the appropriate terminator. (`where`, `with`, `joinWith`, `first`, `count`, `find`, `all`, `paginate` are static shortcuts on the model; the rest live on the query builder, so reach them via `User.query()` or by chaining off a static `where`.)
|
|
2194
2194
|
|
|
@@ -2243,16 +2243,16 @@ const activeUsers = await User.query().scope("active").get();
|
|
|
2243
2243
|
|
|
2244
2244
|
## See also
|
|
2245
2245
|
|
|
2246
|
-
-
|
|
2247
|
-
-
|
|
2248
|
-
-
|
|
2246
|
+
- The `define-relations` topic — `.with(...)`, `.whereHas(...)`, eager loading
|
|
2247
|
+
- The `paginate-results` topic — pagination + cursor + chunk
|
|
2248
|
+
- The `aggregate-data` topic — `.sum`, `.avg`, `.groupBy`, `.having`
|
|
2249
2249
|
|
|
2250
2250
|
|
|
2251
2251
|
## run-cascade-cli `@warlock.js/cascade/run-cascade-cli/SKILL.md`
|
|
2252
2252
|
|
|
2253
2253
|
---
|
|
2254
2254
|
name: run-cascade-cli
|
|
2255
|
-
description: 'Cascade''s standalone `cascade` binary + the Operations API it wraps — `cascade migrate` / `migrate:list` / `migrate:rollback` / `migrate:export-sql`, and `runMigrations` / `rollbackMigrations` / `freshMigrate` / `exportMigrationsSQL` / `listExecutedMigrations` / `createDatabase` / `dropAllTables` / `migrationRunner`. Triggers: `cascade migrate`, `migrate:list`, `migrate:rollback`, `migrate:export-sql`, `runMigrations`, `rollbackMigrations`, `freshMigrate`, `exportMigrationsSQL`, `listExecutedMigrations`, `migrationRunner`; "run migrations in deploy/CI", "reset DB for tests", "programmatic migration", "foreign key constraint cannot be implemented", `CASCADE_PRIMARY_KEY`; typical import `import { runMigrations, migrationRunner } from "@warlock.js/cascade"`. Skip: writing migration files —
|
|
2255
|
+
description: 'Cascade''s standalone `cascade` binary + the Operations API it wraps — `cascade migrate` / `migrate:list` / `migrate:rollback` / `migrate:export-sql`, and `runMigrations` / `rollbackMigrations` / `freshMigrate` / `exportMigrationsSQL` / `listExecutedMigrations` / `createDatabase` / `dropAllTables` / `migrationRunner`. Triggers: `cascade migrate`, `migrate:list`, `migrate:rollback`, `migrate:export-sql`, `runMigrations`, `rollbackMigrations`, `freshMigrate`, `exportMigrationsSQL`, `listExecutedMigrations`, `migrationRunner`; "run migrations in deploy/CI", "reset DB for tests", "programmatic migration", "foreign key constraint cannot be implemented", `CASCADE_PRIMARY_KEY`; typical import `import { runMigrations, migrationRunner } from "@warlock.js/cascade"`. Skip: writing migration files — the `write-migration` topic; competing tools `knex migrate:latest`, `prisma migrate deploy`, `typeorm migration:run`.'
|
|
2256
2256
|
---
|
|
2257
2257
|
|
|
2258
2258
|
# Run the cascade CLI / Operations API
|
|
@@ -2373,15 +2373,15 @@ Reach for these when you need migrations inside test setup (`beforeAll(async ()
|
|
|
2373
2373
|
|
|
2374
2374
|
## See also
|
|
2375
2375
|
|
|
2376
|
-
-
|
|
2377
|
-
-
|
|
2376
|
+
- The `write-migration` topic — writing migration files
|
|
2377
|
+
- The `manage-data-sources` topic — multi-database routing
|
|
2378
2378
|
|
|
2379
2379
|
|
|
2380
2380
|
## search-by-vector `@warlock.js/cascade/search-by-vector/SKILL.md`
|
|
2381
2381
|
|
|
2382
2382
|
---
|
|
2383
2383
|
name: search-by-vector
|
|
2384
|
-
description: 'Vector similarity search via `.similarTo(column, embedding, alias?)` — adds a similarity `score` column and orders by vector distance so the index is used; cap results with `.limit()`. Postgres uses pgvector (IVFFlat index via `this.vectorIndex`); MongoDB needs Atlas. Schema: `this.vector(column, dimensions)` + `this.vectorIndex(column, { dimensions, similarity })`. Triggers: `.similarTo`, `this.vector`, `this.vectorIndex`, `.whereFullText`, pgvector; "semantic search", "RAG retrieval", "find similar articles", "hybrid vector + full-text"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: query basics —
|
|
2384
|
+
description: 'Vector similarity search via `.similarTo(column, embedding, alias?)` — adds a similarity `score` column and orders by vector distance so the index is used; cap results with `.limit()`. Postgres uses pgvector (IVFFlat index via `this.vectorIndex`); MongoDB needs Atlas. Schema: `this.vector(column, dimensions)` + `this.vectorIndex(column, { dimensions, similarity })`. Triggers: `.similarTo`, `this.vector`, `this.vectorIndex`, `.whereFullText`, pgvector; "semantic search", "RAG retrieval", "find similar articles", "hybrid vector + full-text"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: query basics — the `query-data` topic; semantic cache — the `use-cache-similarity` topic of the `warlock-js-cache` skill; competing libs `pgvector` directly, `chromadb`, `pinecone`, `weaviate`, `qdrant`.'
|
|
2385
2385
|
---
|
|
2386
2386
|
|
|
2387
2387
|
# Use vector search
|
|
@@ -2504,15 +2504,15 @@ For local dev with MongoDB, develop the vector path against Postgres + pgvector.
|
|
|
2504
2504
|
|
|
2505
2505
|
## See also
|
|
2506
2506
|
|
|
2507
|
-
-
|
|
2508
|
-
-
|
|
2507
|
+
- The `query-data` topic — `.where`, `.whereFullText`, the broader query vocabulary
|
|
2508
|
+
- The `use-cache-similarity` topic of the `warlock-js-cache` skill — semantic cache of LLM output
|
|
2509
2509
|
|
|
2510
2510
|
|
|
2511
2511
|
## subscribe-to-model-events `@warlock.js/cascade/subscribe-to-model-events/SKILL.md`
|
|
2512
2512
|
|
|
2513
2513
|
---
|
|
2514
2514
|
name: subscribe-to-model-events
|
|
2515
|
-
description: 'Hook into model lifecycle events — `saving` / `saved`, `creating` / `created`, `updating` / `updated`, `validating` / `validated`, `deleting` / `deleted`, `restoring` / `restored`, `fetching` / `fetched`. Per-model `Model.on(event, fn)` or global via `Model.globalEvents()`. Triggers: `Model.on`, `Model.off`, `saving`, `saved`, `created`, `updated`, `deleting`, `deleted`, `restored`; "audit log on save", "notify on change", "denormalize into search index"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: dirty tracking —
|
|
2515
|
+
description: 'Hook into model lifecycle events — `saving` / `saved`, `creating` / `created`, `updating` / `updated`, `validating` / `validated`, `deleting` / `deleted`, `restoring` / `restored`, `fetching` / `fetched`. Per-model `Model.on(event, fn)` or global via `Model.globalEvents()`. Triggers: `Model.on`, `Model.off`, `saving`, `saved`, `created`, `updated`, `deleting`, `deleted`, `restored`; "audit log on save", "notify on change", "denormalize into search index"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: dirty tracking — the `track-changes` topic; competing libs `mongoose` middleware, `typeorm` subscribers, `prisma` extensions.'
|
|
2516
2516
|
---
|
|
2517
2517
|
|
|
2518
2518
|
# Use model events
|
|
@@ -2575,7 +2575,7 @@ User.on("saving", async (user) => {
|
|
|
2575
2575
|
});
|
|
2576
2576
|
```
|
|
2577
2577
|
|
|
2578
|
-
Pair with `isDirty()` from
|
|
2578
|
+
Pair with `isDirty()` from the `track-changes` topic so the listener only runs its expensive check when the relevant field changed.
|
|
2579
2579
|
|
|
2580
2580
|
## Global listeners — across all models
|
|
2581
2581
|
|
|
@@ -2641,7 +2641,7 @@ User.on("deleted", async (user) => {
|
|
|
2641
2641
|
});
|
|
2642
2642
|
```
|
|
2643
2643
|
|
|
2644
|
-
See
|
|
2644
|
+
See the `use-cache-tags` topic of the `warlock-js-cache` skill.
|
|
2645
2645
|
|
|
2646
2646
|
### Side effects that must only fire after commit
|
|
2647
2647
|
|
|
@@ -2656,15 +2656,15 @@ For external side effects (queues, emails) that must only fire if a surrounding
|
|
|
2656
2656
|
|
|
2657
2657
|
## See also
|
|
2658
2658
|
|
|
2659
|
-
-
|
|
2660
|
-
-
|
|
2659
|
+
- The `track-changes` topic — `isDirty` / `getDirtyColumnsWithValues` for "only run if this field changed"
|
|
2660
|
+
- The `configure-delete-strategy` topic — `deleting` / `deleted` / `restored` around delete strategies
|
|
2661
2661
|
|
|
2662
2662
|
|
|
2663
2663
|
## track-changes `@warlock.js/cascade/track-changes/SKILL.md`
|
|
2664
2664
|
|
|
2665
2665
|
---
|
|
2666
2666
|
name: track-changes
|
|
2667
|
-
description: 'Inspect a model''s pending changes — `hasChanges()` (any field dirty?), `isDirty(column)` (one column), `getDirtyColumns()` (changed field names), `getDirtyColumnsWithValues()` (old + new per field), `getRemovedColumns()` (unset fields). Triggers: `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues`, `getRemovedColumns`; "only run if email changed", "diff for an audit log", "what fields are dirty", "compare old vs new value"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: hooking into save —
|
|
2667
|
+
description: 'Inspect a model''s pending changes — `hasChanges()` (any field dirty?), `isDirty(column)` (one column), `getDirtyColumns()` (changed field names), `getDirtyColumnsWithValues()` (old + new per field), `getRemovedColumns()` (unset fields). Triggers: `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues`, `getRemovedColumns`; "only run if email changed", "diff for an audit log", "what fields are dirty", "compare old vs new value"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: hooking into save — the `subscribe-to-model-events` topic; update idioms — the `define-model` topic; competing libs `mongoose` `isModified` / `modifiedPaths`, `typeorm` change detection.'
|
|
2668
2668
|
---
|
|
2669
2669
|
|
|
2670
2670
|
# Track changes
|
|
@@ -2787,15 +2787,15 @@ Before 4.9.1 a `Date` merged over a column that **already held a `Date`** was si
|
|
|
2787
2787
|
|
|
2788
2788
|
## See also
|
|
2789
2789
|
|
|
2790
|
-
-
|
|
2791
|
-
-
|
|
2790
|
+
- The `subscribe-to-model-events` topic — combining with `saved` / `saving` for cross-cutting behavior
|
|
2791
|
+
- The `define-model` topic — the `.set` / `.merge` / `.save` idioms that stage changes
|
|
2792
2792
|
|
|
2793
2793
|
|
|
2794
2794
|
## write-migration `@warlock.js/cascade/write-migration/SKILL.md`
|
|
2795
2795
|
|
|
2796
2796
|
---
|
|
2797
2797
|
name: write-migration
|
|
2798
|
-
description: 'Write a Cascade migration — the declarative `Migration.create(Model, { columns })` / `Migration.alter(Model, { ... })` factory is the primary form (column helpers `string` / `text` / `uuid` / `integer` imported from cascade, chained with `.notNullable()` / `.unique()` / `.references()`); the `extends Migration` class form is the imperative escape hatch. Run with the `cascade migrate` CLI; pin a source via `public dataSource`. Triggers: `Migration.create`, `Migration.alter`, `string()`, `text()`, `uuid()`, `.references`, `extends Migration`, `cascade migrate`; "write a migration", "create the users table", "add a column", "rollback the last batch"; typical import `import { Migration, text, uuid } from "@warlock.js/cascade"`. Skip: running migrations programmatically —
|
|
2798
|
+
description: 'Write a Cascade migration — the declarative `Migration.create(Model, { columns })` / `Migration.alter(Model, { ... })` factory is the primary form (column helpers `string` / `text` / `uuid` / `integer` imported from cascade, chained with `.notNullable()` / `.unique()` / `.references()`); the `extends Migration` class form is the imperative escape hatch. Run with the `cascade migrate` CLI; pin a source via `public dataSource`. Triggers: `Migration.create`, `Migration.alter`, `string()`, `text()`, `uuid()`, `.references`, `extends Migration`, `cascade migrate`; "write a migration", "create the users table", "add a column", "rollback the last batch"; typical import `import { Migration, text, uuid } from "@warlock.js/cascade"`. Skip: running migrations programmatically — the `run-cascade-cli` topic; per-source migrations — the `manage-data-sources` topic; competing tools `knex migrate`, `prisma migrate`, `typeorm migration`.'
|
|
2799
2799
|
---
|
|
2800
2800
|
|
|
2801
2801
|
# Write a migration
|
|
@@ -2842,7 +2842,7 @@ export default Migration.create(User, {
|
|
|
2842
2842
|
- It only fires for the `"soft"` strategy — `"permanent"` and `"trash"` add nothing. No driver defaults to `"soft"`, so this never fires unless soft delete is opted into.
|
|
2843
2843
|
- Opt out for one table with `{ softDeletes: false }`; force it on with `{ softDeletes: true }`. A model with `deletedAtColumn = false` is never wired. An already-declared `deletedAt` in the map is not duplicated.
|
|
2844
2844
|
|
|
2845
|
-
See
|
|
2845
|
+
See the `configure-delete-strategy` topic for the strategies themselves.
|
|
2846
2846
|
|
|
2847
2847
|
## Running migrations
|
|
2848
2848
|
|
|
@@ -2853,7 +2853,7 @@ npx cascade migrate:list # which migrations have been executed
|
|
|
2853
2853
|
npx cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
|
|
2854
2854
|
```
|
|
2855
2855
|
|
|
2856
|
-
`cascade migrate` discovers migration files via the `-p`/`--path` glob (default `./migrations/**`), runs them in order, and records each in the `_migrations` table / collection. See
|
|
2856
|
+
`cascade migrate` discovers migration files via the `-p`/`--path` glob (default `./migrations/**`), runs them in order, and records each in the `_migrations` table / collection. See the `run-cascade-cli` topic for every flag and the programmatic Operations API.
|
|
2857
2857
|
|
|
2858
2858
|
## File naming convention
|
|
2859
2859
|
|
|
@@ -2984,7 +2984,7 @@ export default Migration.create(AnalyticsEvent, {
|
|
|
2984
2984
|
});
|
|
2985
2985
|
```
|
|
2986
2986
|
|
|
2987
|
-
For a class-form migration not bound to a model, set `public readonly dataSource = "analytics"` directly. See
|
|
2987
|
+
For a class-form migration not bound to a model, set `public readonly dataSource = "analytics"` directly. See the `manage-data-sources` topic for the registry.
|
|
2988
2988
|
|
|
2989
2989
|
## Things NOT to do
|
|
2990
2990
|
|
|
@@ -2996,8 +2996,8 @@ For a class-form migration not bound to a model, set `public readonly dataSource
|
|
|
2996
2996
|
|
|
2997
2997
|
## See also
|
|
2998
2998
|
|
|
2999
|
-
-
|
|
3000
|
-
-
|
|
3001
|
-
-
|
|
2999
|
+
- The `run-cascade-cli` topic — CLI flags + Operations API for programmatic runs
|
|
3000
|
+
- The `manage-data-sources` topic — multi-DB migrations
|
|
3001
|
+
- The `configure-delete-strategy` topic — soft / trash / permanent deletes and the `deletedAt` column
|
|
3002
3002
|
|
|
3003
3003
|
|