@warlock.js/cascade 5.19.1 → 5.20.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/cjs/index.cjs +78 -7
- package/cjs/index.cjs.map +1 -1
- package/esm/context/database-transaction-context.d.mts +10 -0
- package/esm/context/database-transaction-context.d.mts.map +1 -1
- package/esm/context/database-transaction-context.mjs +17 -0
- package/esm/context/database-transaction-context.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +13 -5
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +8 -3
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +2 -1
- package/esm/migration/migration-runner.mjs +1 -1
- package/esm/transactions/after-commit.d.mts +24 -0
- package/esm/transactions/after-commit.d.mts.map +1 -0
- package/esm/transactions/after-commit.mjs +47 -0
- package/esm/transactions/after-commit.mjs.map +1 -0
- package/llms-full.txt +2783 -2766
- package/llms.txt +15 -15
- package/package.json +6 -6
- package/skills/aggregate-data/SKILL.md +165 -165
- package/skills/alter-migration/SKILL.md +531 -531
- package/skills/configure-delete-strategy/SKILL.md +130 -130
- package/skills/define-model/SKILL.md +205 -205
- package/skills/define-relations/SKILL.md +171 -171
- package/skills/index.md +23 -0
- package/skills/manage-data-sources/SKILL.md +140 -140
- package/skills/manage-transactions/SKILL.md +208 -191
- package/skills/paginate-results/SKILL.md +122 -122
- package/skills/perform-atomic-ops/SKILL.md +156 -156
- package/skills/query-data/SKILL.md +213 -213
- package/skills/run-cascade-cli/SKILL.md +125 -125
- package/skills/search-by-vector/SKILL.md +127 -127
- package/skills/subscribe-to-model-events/SKILL.md +148 -148
- package/skills/track-changes/SKILL.md +127 -127
- package/skills/write-migration/SKILL.md +206 -206
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: configure-delete-strategy
|
|
3
|
-
description: "
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Use delete strategies
|
|
7
|
-
|
|
8
|
-
`destroy()` does more than `DELETE FROM table`. Cascade supports three strategies that change what happens to the record. Your data source's `defaultDeleteStrategy` applies unless you override per-model or per-call; with nothing configured the fallback is `permanent`.
|
|
9
|
-
|
|
10
|
-
## The three strategies
|
|
11
|
-
|
|
12
|
-
`DeleteStrategy` is `"permanent" | "soft" | "trash"`.
|
|
13
|
-
|
|
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
|
-
|
|
20
|
-
## Set the default per-model
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { Model, RegisterModel } from "@warlock.js/cascade";
|
|
24
|
-
|
|
25
|
-
@RegisterModel()
|
|
26
|
-
export class User extends Model<UserSchema> {
|
|
27
|
-
public static table = "users";
|
|
28
|
-
public static schema = userSchema;
|
|
29
|
-
public static deleteStrategy = "soft" as const; // every destroy() is soft unless overridden
|
|
30
|
-
public static deletedAtColumn = "deletedAt"; // default; set false to disable the column
|
|
31
|
-
}
|
|
32
|
-
```
|
|
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 [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md).
|
|
35
|
-
|
|
36
|
-
## Override per-call
|
|
37
|
-
|
|
38
|
-
```ts
|
|
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
|
-
```
|
|
43
|
-
|
|
44
|
-
Resolution order: `destroy({ strategy })` → `static deleteStrategy` → data source `defaultDeleteStrategy` → `"permanent"`.
|
|
45
|
-
|
|
46
|
-
After a `"soft"` destroy the in-memory instance reflects the change — `model.get(deletedAtColumn)` returns the persisted timestamp (the row stays, so the instance stays usable).
|
|
47
|
-
|
|
48
|
-
## Restoring — static, by id
|
|
49
|
-
|
|
50
|
-
Restore is a **static** operation keyed by the record's id. It auto-detects whether the record was soft-deleted or trashed:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
const user = await User.restore(123); // clears deletedAt or pulls from trash; fires "restored"
|
|
54
|
-
const user2 = await User.restore(123, { onIdConflict: "fail" });
|
|
55
|
-
|
|
56
|
-
await User.restoreAll(); // restore every deleted record for this model
|
|
57
|
-
```
|
|
58
|
-
|
|
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.
|
|
60
|
-
|
|
61
|
-
## Trash strategy — a separate store
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
await user.destroy({ strategy: "trash" }); // moved to the model's trash table, original deleted
|
|
65
|
-
|
|
66
|
-
await User.restore(user.id); // pull back out of the trash table
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
The trash table name resolves as: `static trashTable` on the model → data source `defaultTrashTable` → the `{table}Trash` pattern (e.g. `usersTrash`). Useful when soft-delete clutter would bloat the main table.
|
|
70
|
-
|
|
71
|
-
## Querying soft-deleted records — you filter, Cascade doesn't
|
|
72
|
-
|
|
73
|
-
**Important:** Cascade does **not** auto-hide soft-deleted rows. A plain `User.all()` returns deleted rows too, because the query builder has no built-in `deletedAt` filter. If you want the common "active records only" default, register a global scope yourself:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
@RegisterModel()
|
|
77
|
-
export class User extends Model<UserSchema> {
|
|
78
|
-
public static table = "users";
|
|
79
|
-
public static schema = userSchema;
|
|
80
|
-
public static deleteStrategy = "soft" as const;
|
|
81
|
-
|
|
82
|
-
static {
|
|
83
|
-
// hide soft-deleted rows from every query by default
|
|
84
|
-
this.addGlobalScope("notDeleted", (query) => {
|
|
85
|
-
query.whereNull("deletedAt");
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
With that scope in place:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
await User.all(); // active only (scope applied)
|
|
95
|
-
await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-deleted
|
|
96
|
-
await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) for scopes.
|
|
100
|
-
|
|
101
|
-
## Lifecycle hooks fire on all strategies
|
|
102
|
-
|
|
103
|
-
The delete events are `deleting` / `deleted` (not `destroying` / `destroyed`):
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
User.on("deleting", async (user) => {
|
|
107
|
-
/* about to delete, any strategy */
|
|
108
|
-
});
|
|
109
|
-
User.on("deleted", async (user) => {
|
|
110
|
-
/* delete completed; context carries the strategy + trashRecord for trash */
|
|
111
|
-
});
|
|
112
|
-
User.on("restored", async (user) => {
|
|
113
|
-
/* soft-delete or trash restoration completed */
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
See [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md).
|
|
118
|
-
|
|
119
|
-
## Things NOT to do
|
|
120
|
-
|
|
121
|
-
- Don't switch a model to `soft` without a migration adding the `deletedAt` column. The strategy writes to it.
|
|
122
|
-
- Don't assume soft-deleted rows are hidden automatically — they're not. Add a `notDeleted` global scope (above) or filter `whereNull("deletedAt")` explicitly.
|
|
123
|
-
- Don't call `user.restore()` — restore is static: `User.restore(id)` / `User.restoreAll()`.
|
|
124
|
-
- Don't expect `restore` to undo a `permanent` delete. Hard deletes are gone.
|
|
125
|
-
- Don't use soft delete for GDPR right-to-be-forgotten data. The record stays in the DB; you need `permanent` (plus backup cleanup).
|
|
126
|
-
|
|
127
|
-
## See also
|
|
128
|
-
|
|
129
|
-
- [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md) — `deleting` / `deleted` / `restored` events
|
|
130
|
-
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — global scopes and `withoutGlobalScope` for surfacing deleted rows
|
|
1
|
+
---
|
|
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 — `@warlock.js/cascade/subscribe-to-model-events/SKILL.md`; competing libs `mongoose-delete`, `typeorm softRemove`, `sequelize` paranoid.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Use delete strategies
|
|
7
|
+
|
|
8
|
+
`destroy()` does more than `DELETE FROM table`. Cascade supports three strategies that change what happens to the record. Your data source's `defaultDeleteStrategy` applies unless you override per-model or per-call; with nothing configured the fallback is `permanent`.
|
|
9
|
+
|
|
10
|
+
## The three strategies
|
|
11
|
+
|
|
12
|
+
`DeleteStrategy` is `"permanent" | "soft" | "trash"`.
|
|
13
|
+
|
|
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
|
+
|
|
20
|
+
## Set the default per-model
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Model, RegisterModel } from "@warlock.js/cascade";
|
|
24
|
+
|
|
25
|
+
@RegisterModel()
|
|
26
|
+
export class User extends Model<UserSchema> {
|
|
27
|
+
public static table = "users";
|
|
28
|
+
public static schema = userSchema;
|
|
29
|
+
public static deleteStrategy = "soft" as const; // every destroy() is soft unless overridden
|
|
30
|
+
public static deletedAtColumn = "deletedAt"; // default; set false to disable the column
|
|
31
|
+
}
|
|
32
|
+
```
|
|
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 [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md).
|
|
35
|
+
|
|
36
|
+
## Override per-call
|
|
37
|
+
|
|
38
|
+
```ts
|
|
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
|
+
```
|
|
43
|
+
|
|
44
|
+
Resolution order: `destroy({ strategy })` → `static deleteStrategy` → data source `defaultDeleteStrategy` → `"permanent"`.
|
|
45
|
+
|
|
46
|
+
After a `"soft"` destroy the in-memory instance reflects the change — `model.get(deletedAtColumn)` returns the persisted timestamp (the row stays, so the instance stays usable).
|
|
47
|
+
|
|
48
|
+
## Restoring — static, by id
|
|
49
|
+
|
|
50
|
+
Restore is a **static** operation keyed by the record's id. It auto-detects whether the record was soft-deleted or trashed:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const user = await User.restore(123); // clears deletedAt or pulls from trash; fires "restored"
|
|
54
|
+
const user2 = await User.restore(123, { onIdConflict: "fail" });
|
|
55
|
+
|
|
56
|
+
await User.restoreAll(); // restore every deleted record for this model
|
|
57
|
+
```
|
|
58
|
+
|
|
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.
|
|
60
|
+
|
|
61
|
+
## Trash strategy — a separate store
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
await user.destroy({ strategy: "trash" }); // moved to the model's trash table, original deleted
|
|
65
|
+
|
|
66
|
+
await User.restore(user.id); // pull back out of the trash table
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The trash table name resolves as: `static trashTable` on the model → data source `defaultTrashTable` → the `{table}Trash` pattern (e.g. `usersTrash`). Useful when soft-delete clutter would bloat the main table.
|
|
70
|
+
|
|
71
|
+
## Querying soft-deleted records — you filter, Cascade doesn't
|
|
72
|
+
|
|
73
|
+
**Important:** Cascade does **not** auto-hide soft-deleted rows. A plain `User.all()` returns deleted rows too, because the query builder has no built-in `deletedAt` filter. If you want the common "active records only" default, register a global scope yourself:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
@RegisterModel()
|
|
77
|
+
export class User extends Model<UserSchema> {
|
|
78
|
+
public static table = "users";
|
|
79
|
+
public static schema = userSchema;
|
|
80
|
+
public static deleteStrategy = "soft" as const;
|
|
81
|
+
|
|
82
|
+
static {
|
|
83
|
+
// hide soft-deleted rows from every query by default
|
|
84
|
+
this.addGlobalScope("notDeleted", (query) => {
|
|
85
|
+
query.whereNull("deletedAt");
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With that scope in place:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await User.all(); // active only (scope applied)
|
|
95
|
+
await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-deleted
|
|
96
|
+
await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) for scopes.
|
|
100
|
+
|
|
101
|
+
## Lifecycle hooks fire on all strategies
|
|
102
|
+
|
|
103
|
+
The delete events are `deleting` / `deleted` (not `destroying` / `destroyed`):
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
User.on("deleting", async (user) => {
|
|
107
|
+
/* about to delete, any strategy */
|
|
108
|
+
});
|
|
109
|
+
User.on("deleted", async (user) => {
|
|
110
|
+
/* delete completed; context carries the strategy + trashRecord for trash */
|
|
111
|
+
});
|
|
112
|
+
User.on("restored", async (user) => {
|
|
113
|
+
/* soft-delete or trash restoration completed */
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md).
|
|
118
|
+
|
|
119
|
+
## Things NOT to do
|
|
120
|
+
|
|
121
|
+
- Don't switch a model to `soft` without a migration adding the `deletedAt` column. The strategy writes to it.
|
|
122
|
+
- Don't assume soft-deleted rows are hidden automatically — they're not. Add a `notDeleted` global scope (above) or filter `whereNull("deletedAt")` explicitly.
|
|
123
|
+
- Don't call `user.restore()` — restore is static: `User.restore(id)` / `User.restoreAll()`.
|
|
124
|
+
- Don't expect `restore` to undo a `permanent` delete. Hard deletes are gone.
|
|
125
|
+
- Don't use soft delete for GDPR right-to-be-forgotten data. The record stays in the DB; you need `permanent` (plus backup cleanup).
|
|
126
|
+
|
|
127
|
+
## See also
|
|
128
|
+
|
|
129
|
+
- [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md) — `deleting` / `deleted` / `restored` events
|
|
130
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — global scopes and `withoutGlobalScope` for surfacing deleted rows
|