@warlock.js/cascade 5.30.0 → 5.32.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 (277) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -5
  3. package/cjs/index.cjs +104 -86
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/context/database-data-source-context.d.mts +2 -3
  6. package/esm/context/database-data-source-context.d.mts.map +1 -1
  7. package/esm/context/database-transaction-context.d.mts +2 -3
  8. package/esm/context/database-transaction-context.d.mts.map +1 -1
  9. package/esm/contracts/database-driver.contract.d.mts +56 -36
  10. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  11. package/esm/contracts/database-id-generator.contract.d.mts +7 -5
  12. package/esm/contracts/database-id-generator.contract.d.mts.map +1 -1
  13. package/esm/contracts/database-remover.contract.d.mts +3 -5
  14. package/esm/contracts/database-remover.contract.d.mts.map +1 -1
  15. package/esm/contracts/database-restorer.contract.d.mts +6 -6
  16. package/esm/contracts/database-restorer.contract.d.mts.map +1 -1
  17. package/esm/contracts/database-writer.contract.d.mts +4 -6
  18. package/esm/contracts/database-writer.contract.d.mts.map +1 -1
  19. package/esm/contracts/driver-blueprint.contract.d.mts +2 -3
  20. package/esm/contracts/driver-blueprint.contract.d.mts.map +1 -1
  21. package/esm/contracts/index.d.mts +2 -1
  22. package/esm/contracts/migration-driver.contract.d.mts +101 -53
  23. package/esm/contracts/migration-driver.contract.d.mts.map +1 -1
  24. package/esm/contracts/query-builder.contract.d.mts +51 -33
  25. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  26. package/esm/contracts/sync-adapter.contract.d.mts +25 -14
  27. package/esm/contracts/sync-adapter.contract.d.mts.map +1 -1
  28. package/esm/data-source/data-source-registry.d.mts +4 -5
  29. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  30. package/esm/data-source/data-source-registry.mjs.map +1 -1
  31. package/esm/data-source/data-source.d.mts +8 -5
  32. package/esm/data-source/data-source.d.mts.map +1 -1
  33. package/esm/database-dirty-tracker.d.mts +2 -2
  34. package/esm/database-dirty-tracker.d.mts.map +1 -1
  35. package/esm/database-dirty-tracker.mjs +3 -2
  36. package/esm/database-dirty-tracker.mjs.map +1 -1
  37. package/esm/drivers/mongodb/mongodb-driver.d.mts +3 -4
  38. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  39. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  40. package/esm/drivers/mongodb/mongodb-id-generator.d.mts +2 -3
  41. package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -1
  42. package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -1
  43. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -3
  44. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  45. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +2 -3
  46. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  47. package/esm/drivers/mongodb/mongodb-query-builder.mjs +8 -5
  48. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  49. package/esm/drivers/mongodb/mongodb-query-operations.d.mts +1 -3
  50. package/esm/drivers/mongodb/mongodb-query-operations.d.mts.map +1 -1
  51. package/esm/drivers/mongodb/mongodb-query-operations.mjs.map +1 -1
  52. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +7 -6
  53. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  54. package/esm/drivers/mongodb/mongodb-query-parser.mjs +2 -6
  55. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  56. package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts +1 -3
  57. package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts.map +1 -1
  58. package/esm/drivers/mongodb/types.d.mts +11 -9
  59. package/esm/drivers/mongodb/types.d.mts.map +1 -1
  60. package/esm/drivers/postgres/index.d.mts +2 -1
  61. package/esm/drivers/postgres/index.mjs +1 -1
  62. package/esm/drivers/postgres/naming.mjs +1 -1
  63. package/esm/drivers/postgres/naming.mjs.map +1 -1
  64. package/esm/drivers/postgres/postgres-blueprint.d.mts +1 -3
  65. package/esm/drivers/postgres/postgres-blueprint.d.mts.map +1 -1
  66. package/esm/drivers/postgres/postgres-dialect.d.mts +4 -6
  67. package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
  68. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  69. package/esm/drivers/postgres/postgres-driver.d.mts +4 -5
  70. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  71. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  72. package/esm/drivers/postgres/postgres-migration-driver.d.mts +1 -3
  73. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  74. package/esm/drivers/postgres/postgres-migration-driver.mjs +13 -10
  75. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  76. package/esm/drivers/postgres/postgres-query-builder.d.mts +4 -4
  77. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  78. package/esm/drivers/postgres/postgres-query-builder.mjs +14 -10
  79. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  80. package/esm/drivers/postgres/postgres-query-parser.d.mts +16 -11
  81. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  82. package/esm/drivers/postgres/postgres-query-parser.mjs +1 -2
  83. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  84. package/esm/drivers/postgres/postgres-sql-serializer.mjs +13 -10
  85. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  86. package/esm/drivers/postgres/postgres-ttl-purge.mjs +1 -1
  87. package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -1
  88. package/esm/drivers/postgres/postgres-type-converters.mjs +2 -2
  89. package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -1
  90. package/esm/drivers/postgres/postgres-update-validator.mjs +1 -1
  91. package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -1
  92. package/esm/drivers/postgres/types.d.mts +87 -45
  93. package/esm/drivers/postgres/types.d.mts.map +1 -1
  94. package/esm/drivers/sql/sql-dialect.contract.d.mts +1 -3
  95. package/esm/drivers/sql/sql-dialect.contract.d.mts.map +1 -1
  96. package/esm/drivers/sql/sql-types.d.mts +113 -59
  97. package/esm/drivers/sql/sql-types.d.mts.map +1 -1
  98. package/esm/errors/missing-data-source.error.d.mts +1 -2
  99. package/esm/errors/missing-data-source.error.d.mts.map +1 -1
  100. package/esm/errors/transaction-rollback.error.d.mts +1 -2
  101. package/esm/errors/transaction-rollback.error.d.mts.map +1 -1
  102. package/esm/errors/undefined-where-value.error.d.mts +1 -2
  103. package/esm/errors/undefined-where-value.error.d.mts.map +1 -1
  104. package/esm/errors/unsafe-filter.error.d.mts +1 -2
  105. package/esm/errors/unsafe-filter.error.d.mts.map +1 -1
  106. package/esm/errors/unsafe-raw-expression.error.d.mts +1 -2
  107. package/esm/errors/unsafe-raw-expression.error.d.mts.map +1 -1
  108. package/esm/errors/unsupported-lean-operation.error.d.mts +1 -2
  109. package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -1
  110. package/esm/errors/unsupported-lock-for-update.error.d.mts +1 -2
  111. package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -1
  112. package/esm/errors/unsupported-query-operation.error.d.mts +1 -2
  113. package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -1
  114. package/esm/errors/unsupported-query-operation.error.mjs.map +1 -1
  115. package/esm/errors/unsupported-update-operation.error.d.mts +1 -2
  116. package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -1
  117. package/esm/events/model-events.d.mts +8 -6
  118. package/esm/events/model-events.d.mts.map +1 -1
  119. package/esm/expressions/aggregate-expressions.d.mts +7 -7
  120. package/esm/expressions/aggregate-expressions.d.mts.map +1 -1
  121. package/esm/expressions/column-expressions.d.mts +11 -11
  122. package/esm/expressions/column-expressions.d.mts.map +1 -1
  123. package/esm/expressions/column-expressions.mjs.map +1 -1
  124. package/esm/index.d.mts +7 -2
  125. package/esm/index.mjs +1 -1
  126. package/esm/migration/column-builder.d.mts +2 -3
  127. package/esm/migration/column-builder.d.mts.map +1 -1
  128. package/esm/migration/column-helpers.d.mts +49 -50
  129. package/esm/migration/column-helpers.d.mts.map +1 -1
  130. package/esm/migration/column-helpers.mjs +1 -1
  131. package/esm/migration/foreign-key-builder.d.mts +2 -3
  132. package/esm/migration/foreign-key-builder.d.mts.map +1 -1
  133. package/esm/migration/foreign-key-builder.mjs.map +1 -1
  134. package/esm/migration/index.d.mts +3 -2
  135. package/esm/migration/index.mjs +2 -2
  136. package/esm/migration/migration-runner.d.mts +7 -6
  137. package/esm/migration/migration-runner.d.mts.map +1 -1
  138. package/esm/migration/migration-runner.mjs +15 -11
  139. package/esm/migration/migration-runner.mjs.map +1 -1
  140. package/esm/migration/migration.d.mts +50 -32
  141. package/esm/migration/migration.d.mts.map +1 -1
  142. package/esm/migration/migration.mjs +1 -3
  143. package/esm/migration/migration.mjs.map +1 -1
  144. package/esm/migration/parse-created-at.mjs.map +1 -1
  145. package/esm/migration/sql-grammar.mjs.map +1 -1
  146. package/esm/migration/sql-serializer.d.mts +1 -3
  147. package/esm/migration/sql-serializer.d.mts.map +1 -1
  148. package/esm/migration/types.d.mts +7 -7
  149. package/esm/migration/types.d.mts.map +1 -1
  150. package/esm/model/generate-model-id.mjs.map +1 -1
  151. package/esm/model/methods/accessor-methods.mjs +1 -1
  152. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  153. package/esm/model/methods/dirty-methods.mjs.map +1 -1
  154. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  155. package/esm/model/methods/meta-methods.mjs +4 -2
  156. package/esm/model/methods/meta-methods.mjs.map +1 -1
  157. package/esm/model/methods/query-methods.mjs +1 -1
  158. package/esm/model/methods/query-methods.mjs.map +1 -1
  159. package/esm/model/methods/scope-methods.mjs.map +1 -1
  160. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  161. package/esm/model/methods/write-methods.d.mts +3 -2
  162. package/esm/model/methods/write-methods.d.mts.map +1 -1
  163. package/esm/model/methods/write-methods.mjs +5 -2
  164. package/esm/model/methods/write-methods.mjs.map +1 -1
  165. package/esm/model/model.d.mts +9 -9
  166. package/esm/model/model.d.mts.map +1 -1
  167. package/esm/model/model.mjs.map +1 -1
  168. package/esm/model/model.types.d.mts +7 -7
  169. package/esm/model/model.types.d.mts.map +1 -1
  170. package/esm/model/register-model.d.mts +13 -15
  171. package/esm/model/register-model.d.mts.map +1 -1
  172. package/esm/model/register-model.mjs.map +1 -1
  173. package/esm/model/relation-decorators.d.mts +6 -8
  174. package/esm/model/relation-decorators.d.mts.map +1 -1
  175. package/esm/operations/database.d.mts +13 -8
  176. package/esm/operations/database.d.mts.map +1 -1
  177. package/esm/operations/index.d.mts +2 -1
  178. package/esm/operations/index.mjs +1 -1
  179. package/esm/operations/migrations.d.mts +18 -14
  180. package/esm/operations/migrations.d.mts.map +1 -1
  181. package/esm/operations/migrations.mjs.map +1 -1
  182. package/esm/query-builder/query-builder.d.mts +2 -4
  183. package/esm/query-builder/query-builder.d.mts.map +1 -1
  184. package/esm/query-builder/query-builder.mjs +12 -11
  185. package/esm/query-builder/query-builder.mjs.map +1 -1
  186. package/esm/relations/index.d.mts +2 -1
  187. package/esm/relations/index.mjs +1 -1
  188. package/esm/relations/key-conventions.mjs +2 -1
  189. package/esm/relations/key-conventions.mjs.map +1 -1
  190. package/esm/relations/pivot-operations.d.mts +3 -5
  191. package/esm/relations/pivot-operations.d.mts.map +1 -1
  192. package/esm/relations/pivot-operations.mjs.map +1 -1
  193. package/esm/relations/relation-hydrator.d.mts +3 -5
  194. package/esm/relations/relation-hydrator.d.mts.map +1 -1
  195. package/esm/relations/relation-loader.d.mts +2 -4
  196. package/esm/relations/relation-loader.d.mts.map +1 -1
  197. package/esm/relations/relation-loader.mjs +4 -5
  198. package/esm/relations/relation-loader.mjs.map +1 -1
  199. package/esm/relations/types.d.mts +14 -15
  200. package/esm/relations/types.d.mts.map +1 -1
  201. package/esm/remover/database-remover.d.mts +1 -3
  202. package/esm/remover/database-remover.d.mts.map +1 -1
  203. package/esm/remover/database-remover.mjs.map +1 -1
  204. package/esm/restorer/database-restorer.d.mts +1 -3
  205. package/esm/restorer/database-restorer.d.mts.map +1 -1
  206. package/esm/restorer/database-restorer.mjs +2 -1
  207. package/esm/restorer/database-restorer.mjs.map +1 -1
  208. package/esm/sql-database-dirty-tracker.d.mts +1 -3
  209. package/esm/sql-database-dirty-tracker.d.mts.map +1 -1
  210. package/esm/sync/model-events.mjs +1 -1
  211. package/esm/sync/model-sync-operation.d.mts +2 -4
  212. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  213. package/esm/sync/model-sync.d.mts +3 -4
  214. package/esm/sync/model-sync.d.mts.map +1 -1
  215. package/esm/sync/sync-context.d.mts +2 -4
  216. package/esm/sync/sync-context.d.mts.map +1 -1
  217. package/esm/sync/sync-context.mjs.map +1 -1
  218. package/esm/sync/sync-fanout.mjs +1 -1
  219. package/esm/sync/sync-fanout.mjs.map +1 -1
  220. package/esm/sync/sync-manager.d.mts +2 -4
  221. package/esm/sync/sync-manager.d.mts.map +1 -1
  222. package/esm/sync/sync-manager.mjs.map +1 -1
  223. package/esm/sync/types.d.mts +121 -64
  224. package/esm/sync/types.d.mts.map +1 -1
  225. package/esm/transactions/after-commit.d.mts +2 -2
  226. package/esm/transactions/after-commit.d.mts.map +1 -1
  227. package/esm/types.d.mts +7 -8
  228. package/esm/types.d.mts.map +1 -1
  229. package/esm/utils/connect-to-database.d.mts +7 -8
  230. package/esm/utils/connect-to-database.d.mts.map +1 -1
  231. package/esm/utils/database-writer.utils.d.mts +3 -4
  232. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  233. package/esm/utils/database-writer.utils.mjs.map +1 -1
  234. package/esm/utils/define-model.d.mts +3 -5
  235. package/esm/utils/define-model.d.mts.map +1 -1
  236. package/esm/utils/escape-regex.d.mts +3 -4
  237. package/esm/utils/escape-regex.d.mts.map +1 -1
  238. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  239. package/esm/utils/once-connected.d.mts +2 -4
  240. package/esm/utils/once-connected.d.mts.map +1 -1
  241. package/esm/utils/sanitize-filter.d.mts +3 -4
  242. package/esm/utils/sanitize-filter.d.mts.map +1 -1
  243. package/esm/validation/database-seal-plugins.d.mts +2 -1
  244. package/esm/validation/database-writer-validation-error.d.mts +1 -3
  245. package/esm/validation/database-writer-validation-error.d.mts.map +1 -1
  246. package/esm/validation/index.d.mts +3 -1
  247. package/esm/validation/index.mjs +1 -1
  248. package/esm/validation/plugins/database-rules-plugin.d.mts +2 -3
  249. package/esm/validation/plugins/database-rules-plugin.d.mts.map +1 -1
  250. package/esm/validation/plugins/embed-validator-plugin.d.mts +4 -3
  251. package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
  252. package/esm/validation/types.d.mts +14 -10
  253. package/esm/validation/types.d.mts.map +1 -1
  254. package/esm/validation/validators/embed-validator.d.mts +2 -3
  255. package/esm/validation/validators/embed-validator.d.mts.map +1 -1
  256. package/esm/writer/database-writer.d.mts +1 -3
  257. package/esm/writer/database-writer.d.mts.map +1 -1
  258. package/esm/writer/database-writer.mjs +1 -1
  259. package/esm/writer/database-writer.mjs.map +1 -1
  260. package/llms-full.txt +541 -434
  261. package/package.json +5 -5
  262. package/skills/aggregate-data/SKILL.md +28 -17
  263. package/skills/alter-migration/SKILL.md +325 -233
  264. package/skills/cascade-basics/SKILL.md +19 -19
  265. package/skills/configure-delete-strategy/SKILL.md +11 -11
  266. package/skills/define-model/SKILL.md +19 -17
  267. package/skills/define-relations/SKILL.md +13 -18
  268. package/skills/index.md +3 -0
  269. package/skills/manage-transactions/SKILL.md +18 -12
  270. package/skills/paginate-results/SKILL.md +6 -8
  271. package/skills/perform-atomic-ops/SKILL.md +26 -21
  272. package/skills/query-data/SKILL.md +27 -33
  273. package/skills/run-cascade-cli/SKILL.md +11 -10
  274. package/skills/search-by-vector/SKILL.md +8 -5
  275. package/skills/subscribe-to-model-events/SKILL.md +17 -17
  276. package/skills/track-changes/SKILL.md +9 -9
  277. 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; // generated ID — direct property
60
- user.get("status"); // "active" — default fired
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"); // "ada@example.com"
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… | 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 |
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 | 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)` |
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(); // 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
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); // clears deletedAt or pulls from trash; fires "restored"
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(); // restore every deleted record for this model
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(); // active only (scope applied)
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; // 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
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 | 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. |
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); // ✓ typechecks on both Postgres and MongoDB
74
- shareLink(user.id); // ✗ TS error on SQL: `number` is not assignable to `string`
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 *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()`.
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 | Pattern |
114
- | --- | --- |
115
- | 1–2 specific fields | `user.set(k, v).save()` |
116
- | Bulk from an object | `user.merge(data).save()` |
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", // pivot column → this model (User)
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]); // add (skips existing)
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]); // 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
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"); // already loaded — no second query
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
- .whereHas("posts", (query) => {
109
- query.where("status", "published");
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"); // typed access to the loaded User
135
- post.author; // direct field — only if you declared it as a typed field
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(async () => {
96
- /* ... */
97
- }, { isolationLevel: "SERIALIZABLE" });
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(async () => {
108
- await Invoice.query().get();
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 *different* pending rows with no double-processing and no blocking.
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 *"malformed array literal"*.
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; // User[]
18
- page.pagination; // { total, page, limit, pages }
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; // User[]
41
- first.pagination.nextCursor; // opaque value — pass to the next call
42
- first.pagination.hasMore; // boolean
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({ id: userId }, {
23
- $set: { last_seen: new Date() },
24
- $inc: { login_count: 1 },
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({ slug }, { $set: { "grades.$[low].score": 50 } }, { arrayFilters: [{ "low.score": { $lt: 50 } }] });
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" }); // delete all matching → count
116
- await User.deleteOne({ status: "spam" }); // delete the first match → count
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 | Reach for |
133
- | --- | --- |
134
- | Increment a counter | `Model.increase(filter, field, n)` |
135
- | Atomically change multiple fields on one record | `Model.atomic(filter, ops)` |
136
- | Insert N records | `Model.createMany(rows)` |
137
- | Insert a large batch fast (no per-row events) | `Model.createMany(rows, { bulk: true, batchSize })` |
138
- | Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
139
- | Update one record by id | `Model.update(id, data)` |
140
- | Delete many rows (raw) | `Model.delete(filter)` |
141
- | Multi-row read-modify-write | Wrap in a transaction (see the `manage-transactions` topic) |
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 = 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();
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]); // string form — SQL, real bindings
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); // User | null
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 = await User.first(); // first user, any
78
- const firstAdmin = await User.first({ role: "admin" }); // first admin
79
- const filtered = await User.where("status", "active").first(); // chain into .first()
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 = await User.count();
113
+ const total = await User.count();
120
114
  const activeCount = await User.count({ status: "active" });
121
- const adminCount = await User.where("role", "admin").count();
115
+ const adminCount = await User.where("role", "admin").count();
122
116
 
123
- const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
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 = await User.all();
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 | When |
145
- | --- | --- |
146
- | `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
147
- | `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
148
- | `.whereBetween(field, [a, b])` | Inclusive range |
149
- | `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
150
- | `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
151
- | `.whereHas(relation, callback)` | Filter by conditions on a related model |
152
- | `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — the `aggregate-data` topic |
153
- | `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
154
- | `.chunk(size, callback)` | Stream a large table in batches |
155
- | `.cursorPaginate({ limit, cursor })` | Cursor pagination — the `paginate-results` topic |
156
- | `.similarTo(column, embedding)` | Vector similarity — the `search-by-vector` topic |
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… | Command |
106
- |---|---|
107
- | Run all pending migrations | `cascade migrate` |
108
- | Drop everything and re-run | `cascade migrate -f` |
109
- | Roll back the last batch | `cascade migrate:rollback` |
110
- | Roll back everything | `cascade migrate:rollback --all` |
111
- | Generate SQL without executing | `cascade migrate --sql` |
112
- | Generate SQL for pending only | `cascade migrate --sql --pending-only` |
113
- | See what's been executed | `cascade migrate:list` |
114
- | Run from a non-default folder | `cascade migrate -p "<glob>"` |
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