@warlock.js/cascade 5.30.0 → 5.31.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/README.md +1 -5
- package/cjs/index.cjs +0 -1
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-restorer.contract.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-operations.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-operations.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/naming.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +0 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -1
- package/esm/drivers/postgres/types.d.mts.map +1 -1
- package/esm/errors/unsupported-query-operation.error.mjs.map +1 -1
- package/esm/expressions/column-expressions.d.mts.map +1 -1
- package/esm/expressions/column-expressions.mjs.map +1 -1
- package/esm/migration/foreign-key-builder.mjs.map +1 -1
- package/esm/migration/parse-created-at.mjs.map +1 -1
- package/esm/migration/sql-grammar.mjs.map +1 -1
- package/esm/model/generate-model-id.mjs.map +1 -1
- package/esm/model/methods/accessor-methods.mjs.map +1 -1
- package/esm/model/methods/dirty-methods.mjs.map +1 -1
- package/esm/model/methods/hydration-methods.mjs.map +1 -1
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/methods/scope-methods.mjs.map +1 -1
- package/esm/model/methods/static-event-methods.mjs.map +1 -1
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs.map +1 -1
- package/esm/model/register-model.d.mts.map +1 -1
- package/esm/model/register-model.mjs.map +1 -1
- package/esm/operations/migrations.d.mts.map +1 -1
- package/esm/operations/migrations.mjs.map +1 -1
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/esm/relations/key-conventions.mjs.map +1 -1
- package/esm/relations/pivot-operations.d.mts.map +1 -1
- package/esm/relations/pivot-operations.mjs.map +1 -1
- package/esm/relations/relation-loader.d.mts.map +1 -1
- package/esm/relations/relation-loader.mjs.map +1 -1
- package/esm/remover/database-remover.d.mts.map +1 -1
- package/esm/remover/database-remover.mjs.map +1 -1
- package/esm/sync/sync-context.d.mts.map +1 -1
- package/esm/sync/sync-context.mjs.map +1 -1
- package/esm/sync/sync-fanout.mjs.map +1 -1
- package/esm/sync/sync-manager.d.mts.map +1 -1
- package/esm/sync/sync-manager.mjs.map +1 -1
- package/esm/utils/database-writer.utils.d.mts.map +1 -1
- package/esm/utils/database-writer.utils.mjs.map +1 -1
- package/esm/utils/is-valid-date-value.mjs.map +1 -1
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +541 -434
- package/package.json +5 -5
- package/skills/aggregate-data/SKILL.md +28 -17
- package/skills/alter-migration/SKILL.md +325 -233
- package/skills/cascade-basics/SKILL.md +19 -19
- package/skills/configure-delete-strategy/SKILL.md +11 -11
- package/skills/define-model/SKILL.md +19 -17
- package/skills/define-relations/SKILL.md +13 -18
- package/skills/index.md +3 -0
- package/skills/manage-transactions/SKILL.md +18 -12
- package/skills/paginate-results/SKILL.md +6 -8
- package/skills/perform-atomic-ops/SKILL.md +26 -21
- package/skills/query-data/SKILL.md +27 -33
- package/skills/run-cascade-cli/SKILL.md +11 -10
- package/skills/search-by-vector/SKILL.md +8 -5
- package/skills/subscribe-to-model-events/SKILL.md +17 -17
- package/skills/track-changes/SKILL.md +9 -9
- package/skills/write-migration/SKILL.md +4 -4
|
@@ -56,12 +56,12 @@ export class User extends Model<UserSchema> {
|
|
|
56
56
|
|
|
57
57
|
// Write
|
|
58
58
|
const user = await User.create({ name: "Ada Lovelace", email: "ada@example.com" });
|
|
59
|
-
user.id;
|
|
60
|
-
user.get("status");
|
|
59
|
+
user.id; // generated ID — direct property
|
|
60
|
+
user.get("status"); // "active" — default fired
|
|
61
61
|
|
|
62
62
|
// Read
|
|
63
63
|
const found = await User.find(user.id);
|
|
64
|
-
found?.get("email");
|
|
64
|
+
found?.get("email"); // "ada@example.com"
|
|
65
65
|
|
|
66
66
|
// Filter
|
|
67
67
|
const active = await User.where("status", "active").get();
|
|
@@ -70,22 +70,22 @@ const page = await User.paginate({ page: 1, limit: 20 });
|
|
|
70
70
|
|
|
71
71
|
## Pick a skill
|
|
72
72
|
|
|
73
|
-
| If the task is about…
|
|
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` | The `track-changes` topic
|
|
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
|
|
73
|
+
| If the task is about… | Load |
|
|
74
|
+
| ---------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
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
|
|
|
@@ -11,11 +11,11 @@ description: 'Pick the delete behavior — `permanent` (hard delete), `soft` (se
|
|
|
11
11
|
|
|
12
12
|
`DeleteStrategy` is `"permanent" | "soft" | "trash"`.
|
|
13
13
|
|
|
14
|
-
| Strategy
|
|
15
|
-
|
|
|
16
|
-
| `permanent` | DELETE the row / document (the default fallback)
|
|
17
|
-
| `soft`
|
|
18
|
-
| `trash`
|
|
14
|
+
| Strategy | Behavior | Reversible? |
|
|
15
|
+
| ----------- | -------------------------------------------------------------------------- | ----------------------------- |
|
|
16
|
+
| `permanent` | DELETE the row / document (the default fallback) | No |
|
|
17
|
+
| `soft` | Set the `deletedAt` column; the row stays in the table | Yes — via `Model.restore(id)` |
|
|
18
|
+
| `trash` | Move the record to a separate table / collection, then delete the original | Yes — via `Model.restore(id)` |
|
|
19
19
|
|
|
20
20
|
## Set the default per-model
|
|
21
21
|
|
|
@@ -36,9 +36,9 @@ When the strategy resolves to `"soft"`, `Migration.create(Model, { … })` adds
|
|
|
36
36
|
## Override per-call
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
|
-
await user.destroy();
|
|
40
|
-
await user.destroy({ strategy: "permanent" });
|
|
41
|
-
await user.destroy({ strategy: "trash" });
|
|
39
|
+
await user.destroy(); // uses the model's strategy
|
|
40
|
+
await user.destroy({ strategy: "permanent" }); // GDPR-compliant hard delete
|
|
41
|
+
await user.destroy({ strategy: "trash" }); // move to the trash table
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Resolution order: `destroy({ strategy })` → `static deleteStrategy` → data source `defaultDeleteStrategy` → `"permanent"`.
|
|
@@ -50,10 +50,10 @@ After a `"soft"` destroy the in-memory instance reflects the change — `model.g
|
|
|
50
50
|
Restore is a **static** operation keyed by the record's id. It auto-detects whether the record was soft-deleted or trashed:
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
-
const user = await User.restore(123);
|
|
53
|
+
const user = await User.restore(123); // clears deletedAt or pulls from trash; fires "restored"
|
|
54
54
|
const user2 = await User.restore(123, { onIdConflict: "fail" });
|
|
55
55
|
|
|
56
|
-
await User.restoreAll();
|
|
56
|
+
await User.restoreAll(); // restore every deleted record for this model
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
There is no instance `user.restore()` — call `User.restore(id)`. `restoreAll(options?)` restores all soft-deleted (or all trashed) records for the model's table.
|
|
@@ -91,7 +91,7 @@ export class User extends Model<UserSchema> {
|
|
|
91
91
|
With that scope in place:
|
|
92
92
|
|
|
93
93
|
```ts
|
|
94
|
-
await User.all();
|
|
94
|
+
await User.all(); // active only (scope applied)
|
|
95
95
|
await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-deleted
|
|
96
96
|
await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
|
|
97
97
|
```
|
|
@@ -48,10 +48,10 @@ export class User extends Model<UserSchema> {
|
|
|
48
48
|
## Read state — `.id`, `.uuid`, and `.get(field)`
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
|
-
user.id;
|
|
52
|
-
user.uuid;
|
|
53
|
-
user.get("status");
|
|
54
|
-
user.get<number>("age");
|
|
51
|
+
user.id; // direct property — ID is so common cascade exposes it directly
|
|
52
|
+
user.uuid; // the same id, narrowed to `string` (see below)
|
|
53
|
+
user.get("status"); // canonical reader for every other column
|
|
54
|
+
user.get<number>("age"); // TypeScript generic for typed reads
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Use the direct `.id` property; use `.get("field")` for everything else. Add a typed getter on the model class when the same field is read in many places — turns N typed-cast call sites into one named accessor.
|
|
@@ -60,18 +60,20 @@ Use the direct `.id` property; use `.get("field")` for everything else. Add a ty
|
|
|
60
60
|
|
|
61
61
|
Both getters return the model's primary id (`this.get("id")`); the only difference is the **static type**:
|
|
62
62
|
|
|
63
|
-
| Accessor
|
|
64
|
-
|
|
65
|
-
| `model.id`
|
|
66
|
-
| `model.uuid` | `string`
|
|
63
|
+
| Accessor | Return type | Use when |
|
|
64
|
+
| ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `model.id` | `string \| number` | You want the real id type — SQL auto-increment is `number`, MongoDB's ObjectId is `string`. |
|
|
66
|
+
| `model.uuid` | `string` | You want to pass the id to code typed for a single `string` id, without leaking the engine's `string \| number` union everywhere. |
|
|
67
67
|
|
|
68
68
|
`uuid` does **not** validate or coerce the value to a UUID — the name is historical. It simply returns the same id, typed as `string`, so a helper typed for a string id accepts it on either engine:
|
|
69
69
|
|
|
70
70
|
```ts
|
|
71
|
-
function shareLink(modelId: string) {
|
|
71
|
+
function shareLink(modelId: string) {
|
|
72
|
+
/* ... */
|
|
73
|
+
}
|
|
72
74
|
|
|
73
|
-
shareLink(user.uuid);
|
|
74
|
-
shareLink(user.id);
|
|
75
|
+
shareLink(user.uuid); // ✓ typechecks on both Postgres and MongoDB
|
|
76
|
+
shareLink(user.id); // ✗ TS error on SQL: `number` is not assignable to `string`
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
## Write — three update idioms
|
|
@@ -94,7 +96,7 @@ await user.merge({ name: "Augusta Ada King", status: "active" }).save();
|
|
|
94
96
|
|
|
95
97
|
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
98
|
|
|
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
|
|
99
|
+
**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
100
|
|
|
99
101
|
### `.save()` after manual mutation — when changes are spread
|
|
100
102
|
|
|
@@ -110,11 +112,11 @@ await user.save();
|
|
|
110
112
|
|
|
111
113
|
Quick reference:
|
|
112
114
|
|
|
113
|
-
| Situation
|
|
114
|
-
|
|
|
115
|
-
| 1–2 specific fields
|
|
116
|
-
| Bulk from an object
|
|
117
|
-
| After spread mutations | `user.save()`
|
|
115
|
+
| Situation | Pattern |
|
|
116
|
+
| ---------------------- | ------------------------- |
|
|
117
|
+
| 1–2 specific fields | `user.set(k, v).save()` |
|
|
118
|
+
| Bulk from an object | `user.merge(data).save()` |
|
|
119
|
+
| After spread mutations | `user.save()` |
|
|
118
120
|
|
|
119
121
|
## Remove a field — `.unset(field)`
|
|
120
122
|
|
|
@@ -50,7 +50,7 @@ export class User extends Model<UserSchema> {
|
|
|
50
50
|
|
|
51
51
|
@BelongsToMany("Role", {
|
|
52
52
|
pivot: "user_roles",
|
|
53
|
-
localKey: "user_id",
|
|
53
|
+
localKey: "user_id", // pivot column → this model (User)
|
|
54
54
|
foreignKey: "role_id", // pivot column → the related model (Role)
|
|
55
55
|
})
|
|
56
56
|
public roles!: Role[];
|
|
@@ -64,12 +64,12 @@ export class User extends Model<UserSchema> {
|
|
|
64
64
|
Manage pivot rows through `model.pivot(relation)`, which returns the relation's `PivotOperations` handle:
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
|
-
await user.pivot("roles").attach([adminId, editorId]);
|
|
67
|
+
await user.pivot("roles").attach([adminId, editorId]); // add (skips existing)
|
|
68
68
|
await user.pivot("roles").attach([adminId], { addedBy: actorId }); // + extra pivot columns
|
|
69
|
-
await user.pivot("roles").detach([editorId]);
|
|
70
|
-
await user.pivot("roles").detach();
|
|
71
|
-
await user.pivot("roles").sync([adminId]);
|
|
72
|
-
await user.pivot("roles").toggle([adminId]);
|
|
69
|
+
await user.pivot("roles").detach([editorId]); // remove a subset
|
|
70
|
+
await user.pivot("roles").detach(); // remove all
|
|
71
|
+
await user.pivot("roles").sync([adminId]); // replace the whole set
|
|
72
|
+
await user.pivot("roles").toggle([adminId]); // flip each id
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
`model.attach(relation, ids, pivotData?)` and `model.detach(relation, ids?)` are thin shortcuts for the two most common ops; `.sync()` / `.toggle()` live only on the `pivot(relation)` handle (or the standalone `createPivotOperations(model, relation)`). Passing a non-`belongsToMany` relation throws. Routing through `model.pivot(relation)` keeps the join-table `.sync()` distinct from `Model.sync(Target, field)` (the denormalization-embed feature). Pivot ops run direct driver writes — no model lifecycle events fire on the related model.
|
|
@@ -82,7 +82,7 @@ Load related models in the same query, avoiding N+1:
|
|
|
82
82
|
const posts = await Post.with("author").get();
|
|
83
83
|
|
|
84
84
|
for (const post of posts) {
|
|
85
|
-
post.getRelation("author");
|
|
85
|
+
post.getRelation("author"); // already loaded — no second query
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -104,11 +104,9 @@ await User.with("posts.comments").get();
|
|
|
104
104
|
Filter parents based on conditions on children:
|
|
105
105
|
|
|
106
106
|
```ts
|
|
107
|
-
const usersWithPublishedPosts = await User
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
.get();
|
|
107
|
+
const usersWithPublishedPosts = await User.whereHas("posts", (query) => {
|
|
108
|
+
query.where("status", "published");
|
|
109
|
+
}).get();
|
|
112
110
|
```
|
|
113
111
|
|
|
114
112
|
`.whereHas(relation, callback)` runs the callback on a query builder scoped to the related model. Use when "find Xs that have at least one Y matching Z."
|
|
@@ -131,8 +129,8 @@ Why not `post.set("author", currentUser)`? `set` treats the value as a column wr
|
|
|
131
129
|
```ts
|
|
132
130
|
const post = await Post.with("author").first();
|
|
133
131
|
|
|
134
|
-
post.getRelation("author");
|
|
135
|
-
post.author;
|
|
132
|
+
post.getRelation("author"); // typed access to the loaded User
|
|
133
|
+
post.author; // direct field — only if you declared it as a typed field
|
|
136
134
|
|
|
137
135
|
// Without eager loading — load on demand, then read:
|
|
138
136
|
await post.load("author");
|
|
@@ -146,10 +144,7 @@ const author = post.getRelation("author");
|
|
|
146
144
|
`.with()` runs separate queries and hydrates relations. `.joinWith()` joins at the SQL/aggregation level:
|
|
147
145
|
|
|
148
146
|
```ts
|
|
149
|
-
const result = await User
|
|
150
|
-
.joinWith("posts")
|
|
151
|
-
.where("posts.status", "published")
|
|
152
|
-
.get();
|
|
147
|
+
const result = await User.joinWith("posts").where("posts.status", "published").get();
|
|
153
148
|
```
|
|
154
149
|
|
|
155
150
|
Use `.joinWith` when you need conditions across both tables in a single query. Use `.with` when you want hydrated relation models without joining.
|
package/skills/index.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Model-first ORM for MongoDB and Postgres. Exports `Model`, `RegisterModel`, `Migration`, `defineModel`, `BelongsTo`, `HasMany`, `transaction`, `DataSource`, `dataSourceRegistry`, `connectToDatabase`, `runMigrations`. Use for: define a model, add a column or collection, write or alter a migration, query or filter records, paginate, aggregate or group, add a relation, run a transaction, soft delete, track changes, react to save or delete events, vector search. Not this package: request validation and schemas → @warlock.js/seal; routes, controllers, repositories → @warlock.js/core."
|
|
3
3
|
---
|
|
4
|
+
|
|
4
5
|
# @warlock.js/cascade
|
|
5
6
|
|
|
6
7
|
Cascade is the data layer. The model is the query entry point (`User.find(id)`, `User.paginate(...)`); one seal schema gives the TypeScript type, the save-time validation, and the shape migrations write against. MongoDB and Postgres drivers ship with the same query API. Server-only.
|
|
7
8
|
|
|
8
9
|
## The 80% path
|
|
10
|
+
|
|
9
11
|
1. Read `cascade-basics.md` first: it is the map and the ten foundations.
|
|
10
12
|
2. Define the model and register it (`define-model.md`), then relations (`define-relations.md`).
|
|
11
13
|
3. Write the migration; models never create tables or collections on their own (`write-migration.md`, `alter-migration.md`, run with `run-cascade-cli.md`).
|
|
@@ -14,6 +16,7 @@ Cascade is the data layer. The model is the query entry point (`User.find(id)`,
|
|
|
14
16
|
6. Connect and manage drivers (`manage-data-sources.md`); hook lifecycle and diffs (`subscribe-to-model-events.md`, `track-changes.md`); embeddings via `search-by-vector.md`.
|
|
15
17
|
|
|
16
18
|
## Conventions and pitfalls
|
|
19
|
+
|
|
17
20
|
- `pg` and `mongodb` are optional peers; install only the driver you use.
|
|
18
21
|
- `.create()` validates against the schema, applies defaults, and throws on failure.
|
|
19
22
|
- Pick an update idiom by shape: `.set(k, v).save()` for one or two fields, `.merge(data).save()` for payloads.
|
|
@@ -92,9 +92,12 @@ Throwing works too (and re-throws to the caller); `ctx.rollback()` is the non-th
|
|
|
92
92
|
Default is the driver's default (typically `READ COMMITTED`). Request a different level via `isolationLevel`:
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
|
-
await transaction(
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
await transaction(
|
|
96
|
+
async () => {
|
|
97
|
+
/* ... */
|
|
98
|
+
},
|
|
99
|
+
{ isolationLevel: "SERIALIZABLE" },
|
|
100
|
+
);
|
|
98
101
|
```
|
|
99
102
|
|
|
100
103
|
On `SERIALIZABLE`, Postgres may abort with a serialization failure when concurrent transactions conflict — wrap in retry logic at the caller.
|
|
@@ -104,21 +107,24 @@ On `SERIALIZABLE`, Postgres may abort with a serialization failure when concurre
|
|
|
104
107
|
Pass `settings` to configure PostgreSQL values only for the current transaction. This is useful for row-level security context, timeouts, and schema selection; Cascade applies each value with parameterized `set_config(..., true)`, so it cannot leak back into the pool:
|
|
105
108
|
|
|
106
109
|
```ts
|
|
107
|
-
await transaction(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
settings: {
|
|
111
|
-
"app.tenant_id": tenantId,
|
|
112
|
-
statement_timeout: 5_000,
|
|
110
|
+
await transaction(
|
|
111
|
+
async () => {
|
|
112
|
+
await Invoice.query().get();
|
|
113
113
|
},
|
|
114
|
-
|
|
114
|
+
{
|
|
115
|
+
settings: {
|
|
116
|
+
"app.tenant_id": tenantId,
|
|
117
|
+
statement_timeout: 5_000,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
);
|
|
115
121
|
```
|
|
116
122
|
|
|
117
123
|
## Row locking — `lockForUpdate({ skipLocked })` (Postgres)
|
|
118
124
|
|
|
119
125
|
Chain `lockForUpdate()` on any query to emit `SELECT ... FOR UPDATE`: the matched rows are locked until the transaction ends, so no other transaction can modify or lock them. Only meaningful **inside** a transaction — outside one the lock releases immediately.
|
|
120
126
|
|
|
121
|
-
`{ skipLocked: true }` adds `SKIP LOCKED` — rows another transaction already holds are silently skipped instead of waited on. That is the standard concurrent queue-claim shape: N workers each grab
|
|
127
|
+
`{ skipLocked: true }` adds `SKIP LOCKED` — rows another transaction already holds are silently skipped instead of waited on. That is the standard concurrent queue-claim shape: N workers each grab _different_ pending rows with no double-processing and no blocking.
|
|
122
128
|
|
|
123
129
|
```ts
|
|
124
130
|
import { transaction } from "@warlock.js/cascade";
|
|
@@ -184,7 +190,7 @@ const { rows: counts } = await dataSource.raw<{ count: number }>(
|
|
|
184
190
|
|
|
185
191
|
### Native-array columns (`JSONB[]` / `TEXT[]` / `INTEGER[]`)
|
|
186
192
|
|
|
187
|
-
From a value alone the serializer can't tell a `json` / `jsonb` column (which needs JSON text) from a genuine **native-array** column (`arrayJson()` → `JSONB[]`, `arrayText()` → `TEXT[]`, …, which needs the raw JS array so node-pg emits a `{...}` literal). Guess wrong and Postgres rejects the insert with
|
|
193
|
+
From a value alone the serializer can't tell a `json` / `jsonb` column (which needs JSON text) from a genuine **native-array** column (`arrayJson()` → `JSONB[]`, `arrayText()` → `TEXT[]`, …, which needs the raw JS array so node-pg emits a `{...}` literal). Guess wrong and Postgres rejects the insert with _"malformed array literal"_.
|
|
188
194
|
|
|
189
195
|
**This is handled automatically.** On connect the driver introspects `information_schema` once and remembers which columns are native arrays **per table**, so `arrayText()` / `arrayJson()` columns just work with **no configuration**. Because it's per-table, the same column name being `TEXT[]` in one table and `jsonb` in another is encoded correctly for each; the pgvector all-number array form is still preserved.
|
|
190
196
|
|
|
@@ -14,8 +14,8 @@ The everyday case for listings with page numbers:
|
|
|
14
14
|
```ts
|
|
15
15
|
const page = await User.paginate({ page: 1, limit: 20 });
|
|
16
16
|
|
|
17
|
-
page.data;
|
|
18
|
-
page.pagination;
|
|
17
|
+
page.data; // User[]
|
|
18
|
+
page.pagination; // { total, page, limit, pages }
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
`PaginationOptions` is `{ page?, limit? }` — there is no `filter` field; filter by chaining `.where()` before `.paginate()` (below).
|
|
@@ -23,9 +23,7 @@ page.pagination; // { total, page, limit, pages }
|
|
|
23
23
|
Chain off `.where()` for filtered pagination:
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
const activePage = await User
|
|
27
|
-
.where("status", "active")
|
|
28
|
-
.paginate({ page: 2, limit: 20 });
|
|
26
|
+
const activePage = await User.where("status", "active").paginate({ page: 2, limit: 20 });
|
|
29
27
|
```
|
|
30
28
|
|
|
31
29
|
**Cost characteristic.** Offset pagination scans `offset + limit` rows on every page — page 100 with limit 20 scans 2020 rows just to skip 2000. Fine for the first few pages; not great deep in the result set.
|
|
@@ -37,9 +35,9 @@ For very large datasets where deep pagination matters:
|
|
|
37
35
|
```ts
|
|
38
36
|
const first = await User.query().orderBy("created_at", "desc").cursorPaginate({ limit: 20 });
|
|
39
37
|
|
|
40
|
-
first.data;
|
|
41
|
-
first.pagination.nextCursor;
|
|
42
|
-
first.pagination.hasMore;
|
|
38
|
+
first.data; // User[]
|
|
39
|
+
first.pagination.nextCursor; // opaque value — pass to the next call
|
|
40
|
+
first.pagination.hasMore; // boolean
|
|
43
41
|
|
|
44
42
|
const next = await User.query()
|
|
45
43
|
.orderBy("created_at", "desc")
|
|
@@ -19,10 +19,13 @@ Signature: `Model.increase(filter, field, amount)` / `Model.decrease(filter, fie
|
|
|
19
19
|
## Arbitrary atomic mutations — `Model.atomic`
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
await User.atomic(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
22
|
+
await User.atomic(
|
|
23
|
+
{ id: userId },
|
|
24
|
+
{
|
|
25
|
+
$set: { last_seen: new Date() },
|
|
26
|
+
$inc: { login_count: 1 },
|
|
27
|
+
},
|
|
28
|
+
);
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
`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.
|
|
@@ -48,7 +51,11 @@ const granted = await Quota.atomic(
|
|
|
48
51
|
|
|
49
52
|
// MongoDB only: pipeline update, $addToSet, arrayFilters
|
|
50
53
|
await Post.findOneAndUpdate({ slug }, [{ $set: { score: { $add: ["$likes", "$shares"] } } }]);
|
|
51
|
-
await Post.atomic(
|
|
54
|
+
await Post.atomic(
|
|
55
|
+
{ slug },
|
|
56
|
+
{ $set: { "grades.$[low].score": 50 } },
|
|
57
|
+
{ arrayFilters: [{ "low.score": { $lt: 50 } }] },
|
|
58
|
+
);
|
|
52
59
|
```
|
|
53
60
|
|
|
54
61
|
- `atomic(filter, update, { upsert?, arrayFilters?, trustedFilter? })` → modified + upserted count.
|
|
@@ -56,6 +63,7 @@ await Post.atomic({ slug }, { $set: { "grades.$[low].score": 50 } }, { arrayFilt
|
|
|
56
63
|
- `findAndUpdate(filter, update, { upsert?, arrayFilters? })`. No `trustedFilter`, because it reads the rows back with `where(filter)`.
|
|
57
64
|
|
|
58
65
|
**Postgres.** An upsert runs as `INSERT … ON CONFLICT (target) DO UPDATE … RETURNING *`. The target is the primary key, or a unique index, whose columns are ALL equality keys of the filter. Other filter conditions become `DO UPDATE … WHERE`. If the row exists and fails them, nothing is written and `findOneAndUpdate` returns `null`. The driver throws `UnsupportedUpdateOperationError` (`.operation`, `.driver`) for:
|
|
66
|
+
|
|
59
67
|
- pipeline updates
|
|
60
68
|
- `arrayFilters`
|
|
61
69
|
- `$push`, `$pull`, `$addToSet` and unknown operators
|
|
@@ -98,10 +106,7 @@ const rows = await OrderItem.createMany(millionRows, { bulk: true, batchSize: 10
|
|
|
98
106
|
## Bulk update — `Model.findAndUpdate(filter, operations)`
|
|
99
107
|
|
|
100
108
|
```ts
|
|
101
|
-
const updated = await User.findAndUpdate(
|
|
102
|
-
{ status: "pending" },
|
|
103
|
-
{ $set: { status: "active" } },
|
|
104
|
-
);
|
|
109
|
+
const updated = await User.findAndUpdate({ status: "pending" }, { $set: { status: "active" } });
|
|
105
110
|
// updated: User[] — the matched-and-updated models
|
|
106
111
|
```
|
|
107
112
|
|
|
@@ -112,8 +117,8 @@ const updated = await User.findAndUpdate(
|
|
|
112
117
|
## Bulk delete — `Model.delete(filter)`
|
|
113
118
|
|
|
114
119
|
```ts
|
|
115
|
-
await User.delete({ status: "spam" });
|
|
116
|
-
await User.deleteOne({ status: "spam" });
|
|
120
|
+
await User.delete({ status: "spam" }); // delete all matching → count
|
|
121
|
+
await User.deleteOne({ status: "spam" }); // delete the first match → count
|
|
117
122
|
```
|
|
118
123
|
|
|
119
124
|
`Model.delete(filter?)` and `Model.deleteOne(filter?)` both return `Promise<number>`. These bypass the per-instance delete strategy and `deleted` events — they are raw driver deletes.
|
|
@@ -129,16 +134,16 @@ for (const user of targets) {
|
|
|
129
134
|
|
|
130
135
|
## When to reach for what
|
|
131
136
|
|
|
132
|
-
| Task
|
|
133
|
-
|
|
|
134
|
-
| Increment a counter
|
|
135
|
-
| Atomically change multiple fields on one record | `Model.atomic(filter, ops)`
|
|
136
|
-
| Insert N records
|
|
137
|
-
| Insert a large batch fast (no per-row events)
|
|
138
|
-
| Update many rows with operators
|
|
139
|
-
| Update one record by id
|
|
140
|
-
| Delete many rows (raw)
|
|
141
|
-
| Multi-row read-modify-write
|
|
137
|
+
| Task | Reach for |
|
|
138
|
+
| ----------------------------------------------- | ------------------------------------------------------------- |
|
|
139
|
+
| Increment a counter | `Model.increase(filter, field, n)` |
|
|
140
|
+
| Atomically change multiple fields on one record | `Model.atomic(filter, ops)` |
|
|
141
|
+
| Insert N records | `Model.createMany(rows)` |
|
|
142
|
+
| Insert a large batch fast (no per-row events) | `Model.createMany(rows, { bulk: true, batchSize })` |
|
|
143
|
+
| Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
|
|
144
|
+
| Update one record by id | `Model.update(id, data)` |
|
|
145
|
+
| Delete many rows (raw) | `Model.delete(filter)` |
|
|
146
|
+
| Multi-row read-modify-write | Wrap in a transaction (see the `manage-transactions` topic) |
|
|
142
147
|
| Need lifecycle events / delete strategy per row | `Model.where(...).get()` + iterate + `.save()` / `.destroy()` |
|
|
143
148
|
|
|
144
149
|
## Things NOT to do
|
|
@@ -20,9 +20,9 @@ const activeUsers = await User.where("status", "active").get();
|
|
|
20
20
|
### Operators
|
|
21
21
|
|
|
22
22
|
```ts
|
|
23
|
-
const adults
|
|
24
|
-
const recent
|
|
25
|
-
const nonAdmins
|
|
23
|
+
const adults = await User.where("age", ">", 18).get();
|
|
24
|
+
const recent = await User.where("created_at", ">=", lastWeek).get();
|
|
25
|
+
const nonAdmins = await User.where("role", "!=", "admin").get();
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
3-argument form. Common operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `notIn`, `like`, `between`. Same syntax across MongoDB and Postgres.
|
|
@@ -30,10 +30,7 @@ const nonAdmins = await User.where("role", "!=", "admin").get();
|
|
|
30
30
|
### Compound conditions
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
const activeAdmins = await User
|
|
34
|
-
.where("status", "active")
|
|
35
|
-
.where("role", "admin")
|
|
36
|
-
.get();
|
|
33
|
+
const activeAdmins = await User.where("status", "active").where("role", "admin").get();
|
|
37
34
|
```
|
|
38
35
|
|
|
39
36
|
Chained `.where()` calls combine with `AND`.
|
|
@@ -58,7 +55,7 @@ await User.first({ email, password }); // throws UnsafeFilterError if password i
|
|
|
58
55
|
|
|
59
56
|
```ts
|
|
60
57
|
query.whereRaw({ $expr: { $gt: ["$stock", "$reserved"] } }); // object form — MongoDB
|
|
61
|
-
query.whereRaw("age > ?", [30]);
|
|
58
|
+
query.whereRaw("age > ?", [30]); // string form — SQL, real bindings
|
|
62
59
|
```
|
|
63
60
|
|
|
64
61
|
The **object form** is checked like any other filter value, plus it rejects the server-side-JavaScript operators `$where`, `$function` and `$accumulator` anywhere in the expression (throws `UnsafeRawExpressionError`); `$expr` and the other aggregation operators keep working. The **string form on the MongoDB driver throws `UnsafeRawExpressionError`** — a string used to compile to `{ $where: "<js>" }`, executing arbitrary JavaScript inside `mongod` per scanned document; there is no safe way to parameterize that, so it's rejected outright. Use the object form instead. SQL drivers keep string mode with real `?` bindings (unchanged).
|
|
@@ -68,15 +65,15 @@ The **object form** is checked like any other filter value, plus it rejects the
|
|
|
68
65
|
### By ID
|
|
69
66
|
|
|
70
67
|
```ts
|
|
71
|
-
const user = await User.find(id);
|
|
68
|
+
const user = await User.find(id); // User | null
|
|
72
69
|
```
|
|
73
70
|
|
|
74
71
|
### First match
|
|
75
72
|
|
|
76
73
|
```ts
|
|
77
|
-
const anyUser
|
|
78
|
-
const firstAdmin = await User.first({ role: "admin" });
|
|
79
|
-
const filtered
|
|
74
|
+
const anyUser = await User.first(); // first user, any
|
|
75
|
+
const firstAdmin = await User.first({ role: "admin" }); // first admin
|
|
76
|
+
const filtered = await User.where("status", "active").first(); // chain into .first()
|
|
80
77
|
```
|
|
81
78
|
|
|
82
79
|
`.first()` with no args returns the very first record (driver-dependent default order). With a filter object, the first match by equality. Chain off `.where()` when you need operators.
|
|
@@ -94,10 +91,7 @@ Throws when nothing matches — useful when you KNOW it should exist and want th
|
|
|
94
91
|
## Order and paginate
|
|
95
92
|
|
|
96
93
|
```ts
|
|
97
|
-
const newest = await User
|
|
98
|
-
.where("status", "active")
|
|
99
|
-
.orderBy("created_at", "desc")
|
|
100
|
-
.get();
|
|
94
|
+
const newest = await User.where("status", "active").orderBy("created_at", "desc").get();
|
|
101
95
|
```
|
|
102
96
|
|
|
103
97
|
`.orderBy(field, "asc" | "desc")` sorts. Default direction is `"asc"`. Chain multiple `.orderBy()` for tiebreakers.
|
|
@@ -116,11 +110,11 @@ rows[0].name; // typed as UserSchema, not a User instance
|
|
|
116
110
|
## Count and existence
|
|
117
111
|
|
|
118
112
|
```ts
|
|
119
|
-
const total
|
|
113
|
+
const total = await User.count();
|
|
120
114
|
const activeCount = await User.count({ status: "active" });
|
|
121
|
-
const adminCount
|
|
115
|
+
const adminCount = await User.where("role", "admin").count();
|
|
122
116
|
|
|
123
|
-
const hasAdmin
|
|
117
|
+
const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
|
|
124
118
|
const noneBlocked = await User.where("status", "blocked").notExists();
|
|
125
119
|
```
|
|
126
120
|
|
|
@@ -129,7 +123,7 @@ const noneBlocked = await User.where("status", "blocked").notExists();
|
|
|
129
123
|
## Get many — `.all(filter?)`
|
|
130
124
|
|
|
131
125
|
```ts
|
|
132
|
-
const allUsers
|
|
126
|
+
const allUsers = await User.all();
|
|
133
127
|
const activeUsers = await User.all({ status: "active" });
|
|
134
128
|
```
|
|
135
129
|
|
|
@@ -141,19 +135,19 @@ const activeUsers = await User.all({ status: "active" });
|
|
|
141
135
|
|
|
142
136
|
Cascade's query builder has around 60 methods. Reach for these as the need arises:
|
|
143
137
|
|
|
144
|
-
| Reach for
|
|
145
|
-
|
|
|
146
|
-
| `.whereIn(field, values)` / `.whereNotIn(field, values)`
|
|
147
|
-
| `.whereNull(field)` / `.whereNotNull(field)`
|
|
148
|
-
| `.whereBetween(field, [a, b])`
|
|
149
|
-
| `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers
|
|
150
|
-
| `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch`
|
|
151
|
-
| `.whereHas(relation, callback)`
|
|
152
|
-
| `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)`
|
|
153
|
-
| `.distinct(field)` / `.pluck(field)`
|
|
154
|
-
| `.chunk(size, callback)`
|
|
155
|
-
| `.cursorPaginate({ limit, cursor })`
|
|
156
|
-
| `.similarTo(column, embedding)`
|
|
138
|
+
| Reach for | When |
|
|
139
|
+
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
140
|
+
| `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
|
|
141
|
+
| `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
|
|
142
|
+
| `.whereBetween(field, [a, b])` | Inclusive range |
|
|
143
|
+
| `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
|
|
144
|
+
| `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
|
|
145
|
+
| `.whereHas(relation, callback)` | Filter by conditions on a related model |
|
|
146
|
+
| `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — the `aggregate-data` topic |
|
|
147
|
+
| `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
|
|
148
|
+
| `.chunk(size, callback)` | Stream a large table in batches |
|
|
149
|
+
| `.cursorPaginate({ limit, cursor })` | Cursor pagination — the `paginate-results` topic |
|
|
150
|
+
| `.similarTo(column, embedding)` | Vector similarity — the `search-by-vector` topic |
|
|
157
151
|
|
|
158
152
|
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`.)
|
|
159
153
|
|
|
@@ -19,6 +19,7 @@ cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Flags:
|
|
22
|
+
|
|
22
23
|
- `migrate` — `-f/--fresh` (drop everything and re-run), `-s/--sql` (export SQL instead of executing), `--pending-only`, `-c/--compact`, `-p/--path <glob>`
|
|
23
24
|
- `migrate:rollback` — `-a/--all` (roll back everything), `--batches N`, `-p/--path <glob>`
|
|
24
25
|
- `migrate:export-sql` — `--pending-only`, `-c/--compact`, `-p/--path <glob>`
|
|
@@ -102,16 +103,16 @@ Reach for these when you need migrations inside test setup (`beforeAll(async ()
|
|
|
102
103
|
|
|
103
104
|
## Common task → command
|
|
104
105
|
|
|
105
|
-
| You want to…
|
|
106
|
-
|
|
107
|
-
| Run all pending migrations
|
|
108
|
-
| Drop everything and re-run
|
|
109
|
-
| Roll back the last batch
|
|
110
|
-
| Roll back everything
|
|
111
|
-
| Generate SQL without executing | `cascade migrate --sql`
|
|
112
|
-
| Generate SQL for pending only
|
|
113
|
-
| See what's been executed
|
|
114
|
-
| Run from a non-default folder
|
|
106
|
+
| You want to… | Command |
|
|
107
|
+
| ------------------------------ | -------------------------------------- |
|
|
108
|
+
| Run all pending migrations | `cascade migrate` |
|
|
109
|
+
| Drop everything and re-run | `cascade migrate -f` |
|
|
110
|
+
| Roll back the last batch | `cascade migrate:rollback` |
|
|
111
|
+
| Roll back everything | `cascade migrate:rollback --all` |
|
|
112
|
+
| Generate SQL without executing | `cascade migrate --sql` |
|
|
113
|
+
| Generate SQL for pending only | `cascade migrate --sql --pending-only` |
|
|
114
|
+
| See what's been executed | `cascade migrate:list` |
|
|
115
|
+
| Run from a non-default folder | `cascade migrate -p "<glob>"` |
|
|
115
116
|
|
|
116
117
|
## Things NOT to do
|
|
117
118
|
|