@warlock.js/cascade 5.24.0 → 5.25.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +18 -5
  2. package/README.md +101 -101
  3. package/bin/cascade.js +0 -0
  4. package/cjs/index.cjs +201 -71
  5. package/cjs/index.cjs.map +1 -1
  6. package/esm/contracts/database-driver.contract.d.mts +22 -1
  7. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  8. package/esm/contracts/index.d.mts +1 -1
  9. package/esm/data-source/data-source-registry.d.mts +8 -6
  10. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  11. package/esm/data-source/data-source-registry.mjs +7 -1
  12. package/esm/data-source/data-source-registry.mjs.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-driver.d.mts +13 -0
  14. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-driver.mjs +71 -2
  16. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-query-builder.mjs +2 -1
  19. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  20. package/esm/drivers/postgres/postgres-driver.d.mts +9 -0
  21. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  22. package/esm/drivers/postgres/postgres-driver.mjs +55 -7
  23. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  24. package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -4
  25. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  26. package/esm/drivers/postgres/postgres-query-parser.mjs +4 -5
  27. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  28. package/esm/errors/unsafe-filter.error.mjs.map +1 -1
  29. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
  30. package/esm/errors/unsupported-lock-for-update.error.d.mts +11 -0
  31. package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -0
  32. package/esm/errors/unsupported-lock-for-update.error.mjs +16 -0
  33. package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -0
  34. package/esm/events/model-events.mjs.map +1 -1
  35. package/esm/index.d.mts +3 -2
  36. package/esm/index.mjs +2 -1
  37. package/esm/migration/migration-runner.mjs.map +1 -1
  38. package/esm/migration/sql-grammar.mjs.map +1 -1
  39. package/esm/model/generate-model-id.mjs +23 -0
  40. package/esm/model/generate-model-id.mjs.map +1 -0
  41. package/esm/model/methods/delete-methods.mjs.map +1 -1
  42. package/esm/model/methods/meta-methods.mjs.map +1 -1
  43. package/esm/model/methods/query-methods.mjs +25 -2
  44. package/esm/model/methods/query-methods.mjs.map +1 -1
  45. package/esm/model/methods/write-methods.mjs.map +1 -1
  46. package/esm/query-builder/query-builder.mjs +1 -1
  47. package/esm/query-builder/query-builder.mjs.map +1 -1
  48. package/esm/utils/sanitize-filter.mjs.map +1 -1
  49. package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
  50. package/esm/writer/database-writer.d.mts +0 -33
  51. package/esm/writer/database-writer.d.mts.map +1 -1
  52. package/esm/writer/database-writer.mjs +3 -52
  53. package/esm/writer/database-writer.mjs.map +1 -1
  54. package/llms-full.txt +2901 -2864
  55. package/package.json +5 -5
  56. package/skills/aggregate-data/SKILL.md +165 -165
  57. package/skills/alter-migration/SKILL.md +531 -531
  58. package/skills/configure-delete-strategy/SKILL.md +130 -130
  59. package/skills/define-model/SKILL.md +205 -205
  60. package/skills/define-relations/SKILL.md +171 -171
  61. package/skills/manage-data-sources/SKILL.md +177 -140
  62. package/skills/manage-transactions/SKILL.md +215 -205
  63. package/skills/paginate-results/SKILL.md +122 -122
  64. package/skills/perform-atomic-ops/SKILL.md +156 -156
  65. package/skills/query-data/SKILL.md +213 -213
  66. package/skills/run-cascade-cli/SKILL.md +125 -125
  67. package/skills/search-by-vector/SKILL.md +127 -127
  68. package/skills/subscribe-to-model-events/SKILL.md +148 -148
  69. package/skills/track-changes/SKILL.md +127 -127
  70. package/skills/write-migration/SKILL.md +206 -206
@@ -1,130 +1,130 @@
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
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