@warlock.js/cascade 5.28.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 +6 -0
- 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.txt
CHANGED
|
@@ -6,19 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [aggregate-data](@warlock.js/cascade/aggregate-data/SKILL.md): 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 —
|
|
10
|
-
- [alter-migration](@warlock.js/cascade/alter-migration/SKILL.md): 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 —
|
|
9
|
+
- [aggregate-data](@warlock.js/cascade/aggregate-data/SKILL.md): 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`.
|
|
10
|
+
- [alter-migration](@warlock.js/cascade/alter-migration/SKILL.md): 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.
|
|
11
11
|
- [cascade-basics](@warlock.js/cascade/cascade-basics/SKILL.md): Start here for @warlock.js/cascade models, data sources, migrations, and query workflows.
|
|
12
|
-
- [configure-delete-strategy](@warlock.js/cascade/configure-delete-strategy/SKILL.md): 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 —
|
|
13
|
-
- [define-model](@warlock.js/cascade/define-model/SKILL.md): 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 —
|
|
14
|
-
- [define-relations](@warlock.js/cascade/define-relations/SKILL.md): 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 —
|
|
15
|
-
- [manage-data-sources](@warlock.js/cascade/manage-data-sources/SKILL.md): 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 —
|
|
16
|
-
- [manage-transactions](@warlock.js/cascade/manage-transactions/SKILL.md): 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 —
|
|
17
|
-
- [paginate-results](@warlock.js/cascade/paginate-results/SKILL.md): 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 —
|
|
18
|
-
- [perform-atomic-ops](@warlock.js/cascade/perform-atomic-ops/SKILL.md): 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 —
|
|
19
|
-
- [query-data](@warlock.js/cascade/query-data/SKILL.md): 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 —
|
|
20
|
-
- [run-cascade-cli](@warlock.js/cascade/run-cascade-cli/SKILL.md): 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 —
|
|
21
|
-
- [search-by-vector](@warlock.js/cascade/search-by-vector/SKILL.md): 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 —
|
|
22
|
-
- [subscribe-to-model-events](@warlock.js/cascade/subscribe-to-model-events/SKILL.md): 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 —
|
|
23
|
-
- [track-changes](@warlock.js/cascade/track-changes/SKILL.md): 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 —
|
|
24
|
-
- [write-migration](@warlock.js/cascade/write-migration/SKILL.md): 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 —
|
|
12
|
+
- [configure-delete-strategy](@warlock.js/cascade/configure-delete-strategy/SKILL.md): 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.
|
|
13
|
+
- [define-model](@warlock.js/cascade/define-model/SKILL.md): 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`.
|
|
14
|
+
- [define-relations](@warlock.js/cascade/define-relations/SKILL.md): 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`.
|
|
15
|
+
- [manage-data-sources](@warlock.js/cascade/manage-data-sources/SKILL.md): 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.
|
|
16
|
+
- [manage-transactions](@warlock.js/cascade/manage-transactions/SKILL.md): 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`.
|
|
17
|
+
- [paginate-results](@warlock.js/cascade/paginate-results/SKILL.md): 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`.
|
|
18
|
+
- [perform-atomic-ops](@warlock.js/cascade/perform-atomic-ops/SKILL.md): 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`.
|
|
19
|
+
- [query-data](@warlock.js/cascade/query-data/SKILL.md): 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.
|
|
20
|
+
- [run-cascade-cli](@warlock.js/cascade/run-cascade-cli/SKILL.md): 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`.
|
|
21
|
+
- [search-by-vector](@warlock.js/cascade/search-by-vector/SKILL.md): 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`.
|
|
22
|
+
- [subscribe-to-model-events](@warlock.js/cascade/subscribe-to-model-events/SKILL.md): 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.
|
|
23
|
+
- [track-changes](@warlock.js/cascade/track-changes/SKILL.md): 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.
|
|
24
|
+
- [write-migration](@warlock.js/cascade/write-migration/SKILL.md): 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`.
|
package/package.json
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"@mongez/events": "^2.2.8",
|
|
14
14
|
"@mongez/reinforcements": "^4.0.2",
|
|
15
15
|
"@mongez/supportive-is": "^3.0.0",
|
|
16
|
-
"@warlock.js/context": "5.
|
|
17
|
-
"@warlock.js/logger": "5.
|
|
18
|
-
"@warlock.js/scheduler": "5.
|
|
19
|
-
"@warlock.js/seal": "5.
|
|
16
|
+
"@warlock.js/context": "5.29.0",
|
|
17
|
+
"@warlock.js/logger": "5.29.0",
|
|
18
|
+
"@warlock.js/scheduler": "5.29.0",
|
|
19
|
+
"@warlock.js/seal": "5.29.0",
|
|
20
20
|
"citty": "^0.2.2",
|
|
21
21
|
"fast-glob": "^3.3.3"
|
|
22
22
|
},
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"author": "hassanzohdy",
|
|
54
54
|
"license": "MIT",
|
|
55
|
-
"version": "5.
|
|
55
|
+
"version": "5.29.0",
|
|
56
56
|
"main": "./cjs/index.cjs",
|
|
57
57
|
"module": "./esm/index.mjs",
|
|
58
58
|
"types": "./esm/index.d.mts",
|
package/skills/README.md
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
# `@warlock.js/cascade` — skills index
|
|
2
2
|
|
|
3
|
-
Per-task skills.
|
|
3
|
+
Per-task skills. Cross-references name the topic, for example "the `define-model` topic"; the other package's topics are named with the `warlock-js-<pkg>` skill that holds them.
|
|
4
4
|
|
|
5
5
|
## Skills
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### `alter-migration.md`
|
|
8
8
|
|
|
9
9
|
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()`. Load when changing an existing table — adding columns, swapping unique→index, wiring new foreign keys, or any DDL that's not creating a brand-new table.
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### `aggregate-data.md`
|
|
12
12
|
|
|
13
13
|
Compute aggregates over a query — `.count()`, `.sum(field)`, `.avg`, `.min`, `.max`, the two-arg `.groupBy(fields, { alias: $agg.* })` + `.having`, portable date-bucketing via `.groupByDate(col, unit, aggregates?)`, and expression-aware `$agg.sum(mul("price","quantity"))` / `$agg.sumRaw` for group-level rollups. Load when building reports, dashboards, 'X per category' rollups, time-series ('revenue per month'), or any query returning numbers rather than rows.
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### `cascade-basics.md`
|
|
16
16
|
|
|
17
17
|
Start with @warlock.js/cascade ORM — model-first for MongoDB and Postgres, one schema (seal) does triple duty (type / validator / DB shape), model is the query entry point. Load when importing @warlock.js/cascade, wiring connectToDatabase, defining the first model, or choosing which cascade skill to load.
|
|
18
18
|
|
|
19
|
-
###
|
|
19
|
+
### `configure-delete-strategy.md`
|
|
20
20
|
|
|
21
21
|
Pick the delete behavior — `permanent` (hard delete), `soft` (set deletedAt, keep the row), `trash` (move to a separate table). Configure per-model via `static deleteStrategy` or pass `.destroy({ strategy })` per call; restore via static `Model.restore(id)` / `Model.restoreAll()`. Soft-deleted rows are NOT auto-hidden — add a `notDeleted` global scope yourself. Load when records should be reversibly deletable or implementing GDPR hard delete.
|
|
22
22
|
|
|
23
|
-
###
|
|
23
|
+
### `define-model.md`
|
|
24
24
|
|
|
25
25
|
Define a Cascade model — @RegisterModel() decorator, 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 why merge() on a persisted model can't retarget a write via a spoofed id, and why static hidden — not toJsonColumns/resource alone — is the field that's guaranteed to never serialize. Load when creating a model file, adding accessors, shaping serialized output, or hiding a password/token from responses.
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### `define-relations.md`
|
|
28
28
|
|
|
29
29
|
Define and query relations — @BelongsTo('Model') / @HasMany('Model') / @BelongsToMany('Model', { pivot, localKey, foreignKey }), .with('relation') eager loading, .whereHas(relation, cb) filter-by-related, setRelation on save, .joinWith for SQL joins, .load(relation) for lazy loading. Load when two models reference each other, avoiding N+1, or filtering parents by child conditions.
|
|
30
30
|
|
|
31
|
-
###
|
|
31
|
+
### `manage-data-sources.md`
|
|
32
32
|
|
|
33
33
|
Configure multiple databases — register each via `connectToDatabase({ name, driver, database, isDefault })`, assign a model with `static dataSource = 'name'`, pin a migration with `public dataSource`, inspect via `dataSourceRegistry.get(name)` / `getAllDataSources()`. Load when the app talks to more than one database (reporting replica, analytics DB, per-tenant DBs).
|
|
34
34
|
|
|
35
|
-
###
|
|
35
|
+
### `manage-transactions.md`
|
|
36
36
|
|
|
37
37
|
Wrap multi-statement work in transaction(async () => {...}) — rollback on throw or `ctx.rollback()`, commit on resolve, `isolationLevel` option (Postgres). Postgres native, MongoDB requires replica set; nested calls flat-join the outer transaction. Also covers row locking (`lockForUpdate({ skipLocked })` → `FOR UPDATE SKIP LOCKED`, Postgres-only — the concurrent queue-claim shape), transaction-aware raw SQL (`Model.raw` / `DataSource.raw` → `RawQueryResult`) and the Postgres `nativeArrayColumns` connection option. Load when two or more writes must succeed or fail together (creating parent + children, transferring balances, multi-step state machines), locking rows against concurrent workers, or when running raw SQL.
|
|
38
38
|
|
|
39
|
-
###
|
|
39
|
+
### `paginate-results.md`
|
|
40
40
|
|
|
41
41
|
Paginate query results — `.paginate({ page, limit })` for offset (returns `data` + `pagination`), `.cursorPaginate({ limit, cursor })` for very large datasets (cursor fields under `pagination`), `.chunk(size, callback)` for streaming. Load when listing records for a UI, fetching a slice for an API, or processing a large table in batches.
|
|
42
42
|
|
|
43
|
-
###
|
|
43
|
+
### `perform-atomic-ops.md`
|
|
44
44
|
|
|
45
45
|
Avoid races on concurrent writes — Model.increase(filter, field, n) / Model.decrease for atomic counters, Model.atomic(filter, ops) for arbitrary atomic mutations, Model.createMany(rows, { bulk, batchSize }) / Model.findAndUpdate / Model.delete for bulk. atomic() / findAndUpdate() / findOneAndUpdate() / findAndReplace() / findOneAndDelete() sanitize their filter argument the same way where() does. Load when incrementing counters under concurrency, fast bulk inserts (native multi-row insertMany), bulk ops without N+1, or single-document atomic mutations without a full transaction.
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### `query-data.md`
|
|
48
48
|
|
|
49
49
|
Query records via the model — .where(field, value) / .where(field, op, value), .find(id) / .first(filter?) / .all(filter?), .count / .exists, plus the broader query-builder vocabulary (.orderBy / .whereIn / .whereBetween / .whereLike / .whereRaw / .pluck / .firstOrFail / scopes) via .query() or chained off .where(). Covers filter safety (where() rejects $-prefixed keys, whereRaw() string mode rejects $where, whereLike/whereSearch match strings literally). Load when filtering, fetching by ID, getting a single match, ordering, or checking whether a filter/search built from request data is safe.
|
|
50
50
|
|
|
51
|
-
###
|
|
51
|
+
### `run-cascade-cli.md`
|
|
52
52
|
|
|
53
53
|
Cascade's standalone CLI binary + programmatic Operations API — cascade migrate / migrate:list / migrate:rollback / migrate:export-sql, plus runMigrations / rollbackMigrations / freshMigrate / exportMigrationsSQL / listExecutedMigrations / migrationRunner functions. Load when applying migrations during deploy, resetting DB for test setup, or programmatic migration ops from a custom script.
|
|
54
54
|
|
|
55
|
-
###
|
|
55
|
+
### `search-by-vector.md`
|
|
56
56
|
|
|
57
57
|
Vector similarity search via `.similarTo(column, embedding, alias?)` — adds a `score` column, orders by distance for index usage, cap with `.limit()`. Postgres uses pgvector (IVFFlat index via `this.vectorIndex`); MongoDB uses Atlas. Load when building semantic search, RAG retrieval against documents, hybrid (vector + full-text) search, or 'find records most similar to this vector'.
|
|
58
58
|
|
|
59
|
-
###
|
|
59
|
+
### `subscribe-to-model-events.md`
|
|
60
60
|
|
|
61
61
|
Hook into model lifecycle events — `saving` / `saved`, `creating` / `created`, `updating` / `updated`, `validating` / `validated`, `deleting` / `deleted`, `restoring` / `restored`, `fetching` / `fetched`. The gerund form is the "before" hook (no `beforeSave` alias). Per-model `Model.on` or global. Load when wiring an audit log, sending notifications on changes, or denormalizing into a search index.
|
|
62
62
|
|
|
63
|
-
###
|
|
63
|
+
### `track-changes.md`
|
|
64
64
|
|
|
65
65
|
Inspect a model's pending changes — `hasChanges()`, `isDirty(column)`, `getDirtyColumns()` (changed field names), `getDirtyColumnsWithValues()` (old + new per field), `getRemovedColumns()`. Load when implementing audit logs, conditionally running expensive logic only if a field changed, or building a change diff.
|
|
66
66
|
|
|
67
|
-
###
|
|
67
|
+
### `write-migration.md`
|
|
68
68
|
|
|
69
69
|
Write a Cascade migration — a class `extends Migration` with `public table`, `up()` / `down()`, and fluent schema builders on `this` (`createTable`, `id`, `string`, `timestamps`, `references`, `index`). Run with `cascade migrate`. Load when creating or altering a table, indexing a column, dropping data, or wiring a new model.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aggregate-data
|
|
3
|
-
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 —
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use aggregates and groupBy
|
|
@@ -161,5 +161,5 @@ Raw stages: `joinRaw({ $lookup: { from, let, pipeline, as } })` (or an array of
|
|
|
161
161
|
|
|
162
162
|
## See also
|
|
163
163
|
|
|
164
|
-
-
|
|
165
|
-
-
|
|
164
|
+
- The `query-data` topic — the broader query vocabulary
|
|
165
|
+
- The `use-cached-hof` topic of the `warlock-js-cache` skill — caching expensive aggregate results
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: alter-migration
|
|
3
|
-
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 —
|
|
3
|
+
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.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Alter a migration
|
|
@@ -526,6 +526,6 @@ export default Migration.alter(Product, {
|
|
|
526
526
|
|
|
527
527
|
## See also
|
|
528
528
|
|
|
529
|
-
-
|
|
530
|
-
-
|
|
531
|
-
-
|
|
529
|
+
- The `write-migration` topic — `Migration.create` for brand-new tables + all column helpers
|
|
530
|
+
- The `run-cascade-cli` topic — CLI flags + programmatic Operations API
|
|
531
|
+
- The `manage-data-sources` topic — multi-DB migrations
|
|
@@ -24,15 +24,15 @@ npm install @warlock.js/cascade @warlock.js/seal
|
|
|
24
24
|
The 10 things that are true in every cascade use:
|
|
25
25
|
|
|
26
26
|
1. **The model is the query entry point.** `User.where(...)`, `User.find(id)`, `User.paginate(...)`. No `db.users`, no separate client, no repository layer.
|
|
27
|
-
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
|
|
27
|
+
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.
|
|
28
28
|
3. **`@RegisterModel()` puts the model in the global registry.** Other models look it up by name for relations (`@BelongsTo("User")` or `@BelongsTo(lazy(() => User))`).
|
|
29
29
|
4. **Two drivers ship in-box: MongoDB and Postgres.** Same query API across both. Switch via config; the call sites stay identical.
|
|
30
|
-
5. **Migrations are required.** `cascade migrate` runs schema changes; the model class doesn't auto-create tables. See
|
|
31
|
-
6. **`.create()` validates against the schema before persisting.** Defaults (`v.string().default(...)`) fire here. Validation errors throw — see
|
|
32
|
-
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
|
|
33
|
-
8. **`.destroy()` runs the configured delete strategy** (`permanent` / `soft` / `trash`). See
|
|
34
|
-
9. **Lifecycle events fire on every meaningful moment** — `saving` / `saved`, `creating` / `created`, `deleting` / `deleted`. Hook on the model class. See
|
|
35
|
-
10. **Transactions are first-class.** `transaction(async () => { ... })` wraps a unit; rollback on throw, commit on resolve. See
|
|
30
|
+
5. **Migrations are required.** `cascade migrate` runs schema changes; the model class doesn't auto-create tables. See the `write-migration` topic.
|
|
31
|
+
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.
|
|
32
|
+
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.
|
|
33
|
+
8. **`.destroy()` runs the configured delete strategy** (`permanent` / `soft` / `trash`). See the `configure-delete-strategy` topic.
|
|
34
|
+
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.
|
|
35
|
+
10. **Transactions are first-class.** `transaction(async () => { ... })` wraps a unit; rollback on throw, commit on resolve. See the `manage-transactions` topic.
|
|
36
36
|
|
|
37
37
|
## Minimal example — model, write, read
|
|
38
38
|
|
|
@@ -72,20 +72,20 @@ const page = await User.paginate({ page: 1, limit: 20 });
|
|
|
72
72
|
|
|
73
73
|
| If the task is about… | Load |
|
|
74
74
|
| --- | --- |
|
|
75
|
-
| Defining a model class — schema, decorators, `Model<TSchema>`, accessors |
|
|
76
|
-
| Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering |
|
|
77
|
-
| Pagination — `.paginate`, `cursorPaginate`, `chunk` |
|
|
78
|
-
| Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading |
|
|
79
|
-
| Migrations — `migration` definition, `up`/`down`, CLI |
|
|
80
|
-
| Transactions — `transaction(fn)`, rollback, isolation |
|
|
81
|
-
| Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` |
|
|
82
|
-
| Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` |
|
|
83
|
-
| Soft / hard / trash deletes + restore |
|
|
84
|
-
| Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` |
|
|
85
|
-
| Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` |
|
|
86
|
-
| Vector search — `similarTo`, pgvector / Atlas vector index |
|
|
87
|
-
| Multiple databases — `connectToDatabase`, per-model `static dataSource` |
|
|
88
|
-
| CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic |
|
|
75
|
+
| Defining a model class — schema, decorators, `Model<TSchema>`, accessors | The `define-model` topic |
|
|
76
|
+
| Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering | The `query-data` topic |
|
|
77
|
+
| Pagination — `.paginate`, `cursorPaginate`, `chunk` | The `paginate-results` topic |
|
|
78
|
+
| Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading | The `define-relations` topic |
|
|
79
|
+
| Migrations — `migration` definition, `up`/`down`, CLI | The `write-migration` topic |
|
|
80
|
+
| Transactions — `transaction(fn)`, rollback, isolation | The `manage-transactions` topic |
|
|
81
|
+
| Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` | The `track-changes` topic |
|
|
82
|
+
| Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` | The `subscribe-to-model-events` topic |
|
|
83
|
+
| Soft / hard / trash deletes + restore | The `configure-delete-strategy` topic |
|
|
84
|
+
| Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` | The `perform-atomic-ops` topic |
|
|
85
|
+
| Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` | The `aggregate-data` topic |
|
|
86
|
+
| Vector search — `similarTo`, pgvector / Atlas vector index | The `search-by-vector` topic |
|
|
87
|
+
| Multiple databases — `connectToDatabase`, per-model `static dataSource` | The `manage-data-sources` topic |
|
|
88
|
+
| CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic | The `run-cascade-cli` topic |
|
|
89
89
|
|
|
90
90
|
## Things NOT to do
|
|
91
91
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: configure-delete-strategy
|
|
3
|
-
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 —
|
|
3
|
+
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.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use delete strategies
|
|
@@ -31,7 +31,7 @@ export class User extends Model<UserSchema> {
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
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
|
|
34
|
+
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.
|
|
35
35
|
|
|
36
36
|
## Override per-call
|
|
37
37
|
|
|
@@ -96,7 +96,7 @@ await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-dele
|
|
|
96
96
|
await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See
|
|
99
|
+
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See the `query-data` topic for scopes.
|
|
100
100
|
|
|
101
101
|
## Lifecycle hooks fire on all strategies
|
|
102
102
|
|
|
@@ -114,7 +114,7 @@ User.on("restored", async (user) => {
|
|
|
114
114
|
});
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
See
|
|
117
|
+
See the `subscribe-to-model-events` topic.
|
|
118
118
|
|
|
119
119
|
## Things NOT to do
|
|
120
120
|
|
|
@@ -126,5 +126,5 @@ See [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/casca
|
|
|
126
126
|
|
|
127
127
|
## See also
|
|
128
128
|
|
|
129
|
-
-
|
|
130
|
-
-
|
|
129
|
+
- The `subscribe-to-model-events` topic — `deleting` / `deleted` / `restored` events
|
|
130
|
+
- The `query-data` topic — global scopes and `withoutGlobalScope` for surfacing deleted rows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: define-model
|
|
3
|
-
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 —
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Define a model
|
|
@@ -26,7 +26,7 @@ export type UserSchema = Infer<typeof userSchema>;
|
|
|
26
26
|
- Fields are **required by default** — chain `.optional()` on any field that may be missing.
|
|
27
27
|
- `Infer<typeof userSchema>` derives the TS type. No second declaration, no drift.
|
|
28
28
|
- The schema is standalone — reuse it for HTTP body validation, service-input validation, anywhere else.
|
|
29
|
-
- See
|
|
29
|
+
- See the `seal-basics` topic of the `warlock-js-seal` skill for the validator vocabulary.
|
|
30
30
|
|
|
31
31
|
## Step 2 — Define the model class
|
|
32
32
|
|
|
@@ -94,7 +94,7 @@ await user.merge({ name: "Augusta Ada King", status: "active" }).save();
|
|
|
94
94
|
|
|
95
95
|
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.
|
|
96
96
|
|
|
97
|
-
**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
|
|
97
|
+
**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()`.
|
|
98
98
|
|
|
99
99
|
### `.save()` after manual mutation — when changes are spread
|
|
100
100
|
|
|
@@ -130,7 +130,7 @@ await user.unset("image").save();
|
|
|
130
130
|
await user.destroy();
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
Runs the model's lifecycle (events, configured delete strategy), then removes the record. See
|
|
133
|
+
Runs the model's lifecycle (events, configured delete strategy), then removes the record. See the `configure-delete-strategy` topic for soft / hard / trash semantics.
|
|
134
134
|
|
|
135
135
|
## Public output shaping
|
|
136
136
|
|
|
@@ -199,7 +199,7 @@ export class User extends Model<UserSchema> {
|
|
|
199
199
|
|
|
200
200
|
## See also
|
|
201
201
|
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
202
|
+
- The `query-data` topic — finding and filtering records
|
|
203
|
+
- The `define-relations` topic — relations and eager loading
|
|
204
|
+
- The `track-changes` topic — dirty tracking
|
|
205
|
+
- The `subscribe-to-model-events` topic — lifecycle hooks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: define-relations
|
|
3
|
-
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 —
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Define and query relations
|
|
@@ -167,5 +167,5 @@ For embedded/denormalized relations (cached `author_name` on posts, etc.), Casca
|
|
|
167
167
|
|
|
168
168
|
## See also
|
|
169
169
|
|
|
170
|
-
-
|
|
171
|
-
-
|
|
170
|
+
- The `query-data` topic — `.whereHas` and other filters
|
|
171
|
+
- The `define-model` topic — `@RegisterModel`, the registry that makes name-based lookup work
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: manage-data-sources
|
|
3
|
-
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 —
|
|
3
|
+
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.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Manage data sources
|
|
@@ -86,7 +86,7 @@ export default class CreateEventsTable extends Migration {
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
See
|
|
89
|
+
See the `write-migration` topic for the migration shape.
|
|
90
90
|
|
|
91
91
|
## Transactions and data sources
|
|
92
92
|
|
|
@@ -173,5 +173,5 @@ unless `clientOptions.monitorCommands` is explicitly set.
|
|
|
173
173
|
|
|
174
174
|
## See also
|
|
175
175
|
|
|
176
|
-
-
|
|
177
|
-
-
|
|
176
|
+
- The `write-migration` topic — per-source migrations
|
|
177
|
+
- The `manage-transactions` topic — transactions run on the default source
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: manage-transactions
|
|
3
|
-
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 —
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use transactions
|
|
@@ -152,7 +152,7 @@ const claimed = await Job.atomic(
|
|
|
152
152
|
);
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
See
|
|
155
|
+
See the `perform-atomic-ops` topic for `atomic` / `findOneAndUpdate`.
|
|
156
156
|
|
|
157
157
|
## Outside the transaction
|
|
158
158
|
|
|
@@ -229,5 +229,5 @@ Use the outbox pattern above instead when the side effect must survive a crash b
|
|
|
229
229
|
|
|
230
230
|
## See also
|
|
231
231
|
|
|
232
|
-
-
|
|
233
|
-
-
|
|
232
|
+
- The `perform-atomic-ops` topic — atomic single-document ops without a full transaction
|
|
233
|
+
- The `manage-data-sources` topic — transactions run on the default source
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: paginate-results
|
|
3
|
-
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 —
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Paginate results
|
|
@@ -89,7 +89,7 @@ Eager-load relations on a paginated page:
|
|
|
89
89
|
const page = await Post.with("author").paginate({ page: 1, limit: 20 });
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
See
|
|
92
|
+
See the `define-relations` topic.
|
|
93
93
|
|
|
94
94
|
## Pagination shape
|
|
95
95
|
|
|
@@ -118,5 +118,5 @@ The total count requires an extra query. On very large filtered tables, this can
|
|
|
118
118
|
|
|
119
119
|
## See also
|
|
120
120
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
121
|
+
- The `query-data` topic — `.where`, `.orderBy`, filter chains
|
|
122
|
+
- The `define-relations` topic — eager loading on a paginated page
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: perform-atomic-ops
|
|
3
|
-
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 —
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use atomic operations
|
|
@@ -27,7 +27,7 @@ await User.atomic({ id: userId }, {
|
|
|
27
27
|
|
|
28
28
|
`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.
|
|
29
29
|
|
|
30
|
-
**`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
|
|
30
|
+
**`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).
|
|
31
31
|
|
|
32
32
|
## Upsert, returnDocument, pipelines — the options argument
|
|
33
33
|
|
|
@@ -138,7 +138,7 @@ for (const user of targets) {
|
|
|
138
138
|
| Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
|
|
139
139
|
| Update one record by id | `Model.update(id, data)` |
|
|
140
140
|
| Delete many rows (raw) | `Model.delete(filter)` |
|
|
141
|
-
| Multi-row read-modify-write | Wrap in a
|
|
141
|
+
| Multi-row read-modify-write | Wrap in a transaction (see the `manage-transactions` topic) |
|
|
142
142
|
| Need lifecycle events / delete strategy per row | `Model.where(...).get()` + iterate + `.save()` / `.destroy()` |
|
|
143
143
|
|
|
144
144
|
## Things NOT to do
|
|
@@ -152,5 +152,5 @@ for (const user of targets) {
|
|
|
152
152
|
|
|
153
153
|
## See also
|
|
154
154
|
|
|
155
|
-
-
|
|
156
|
-
-
|
|
155
|
+
- The `manage-transactions` topic — multi-row atomicity
|
|
156
|
+
- The `paginate-results` topic — `.chunk` for bulk-processing iteration
|