@warlock.js/cascade 5.29.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.
Files changed (392) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -5
  3. package/cjs/index.cjs +101 -102
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/context/database-data-source-context.d.mts +1 -1
  6. package/esm/context/database-data-source-context.d.mts.map +1 -1
  7. package/esm/context/database-data-source-context.mjs +1 -1
  8. package/esm/context/database-data-source-context.mjs.map +1 -1
  9. package/esm/context/database-transaction-context.d.mts +1 -1
  10. package/esm/context/database-transaction-context.d.mts.map +1 -1
  11. package/esm/context/database-transaction-context.mjs +1 -1
  12. package/esm/context/database-transaction-context.mjs.map +1 -1
  13. package/esm/contracts/database-driver.contract.d.mts +1 -1
  14. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  15. package/esm/contracts/database-id-generator.contract.d.mts +1 -1
  16. package/esm/contracts/database-id-generator.contract.d.mts.map +1 -1
  17. package/esm/contracts/database-remover.contract.d.mts +1 -1
  18. package/esm/contracts/database-remover.contract.d.mts.map +1 -1
  19. package/esm/contracts/database-restorer.contract.d.mts +1 -1
  20. package/esm/contracts/database-restorer.contract.d.mts.map +1 -1
  21. package/esm/contracts/database-writer.contract.d.mts +1 -1
  22. package/esm/contracts/database-writer.contract.d.mts.map +1 -1
  23. package/esm/contracts/driver-blueprint.contract.d.mts +1 -1
  24. package/esm/contracts/driver-blueprint.contract.d.mts.map +1 -1
  25. package/esm/contracts/migration-driver.contract.d.mts +1 -1
  26. package/esm/contracts/migration-driver.contract.d.mts.map +1 -1
  27. package/esm/contracts/query-builder.contract.d.mts +1 -1
  28. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  29. package/esm/contracts/sync-adapter.contract.d.mts +1 -1
  30. package/esm/contracts/sync-adapter.contract.d.mts.map +1 -1
  31. package/esm/data-source/data-source-registry.d.mts +1 -1
  32. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  33. package/esm/data-source/data-source-registry.mjs +1 -1
  34. package/esm/data-source/data-source-registry.mjs.map +1 -1
  35. package/esm/data-source/data-source.d.mts +1 -1
  36. package/esm/data-source/data-source.d.mts.map +1 -1
  37. package/esm/data-source/data-source.mjs +1 -1
  38. package/esm/data-source/data-source.mjs.map +1 -1
  39. package/esm/database-dirty-tracker.d.mts +1 -1
  40. package/esm/database-dirty-tracker.d.mts.map +1 -1
  41. package/esm/database-dirty-tracker.mjs +1 -1
  42. package/esm/database-dirty-tracker.mjs.map +1 -1
  43. package/esm/drivers/mongodb/mongodb-blueprint.mjs +1 -1
  44. package/esm/drivers/mongodb/mongodb-blueprint.mjs.map +1 -1
  45. package/esm/drivers/mongodb/mongodb-driver.d.mts +1 -1
  46. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  47. package/esm/drivers/mongodb/mongodb-driver.mjs +1 -1
  48. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  49. package/esm/drivers/mongodb/mongodb-id-generator.d.mts +1 -1
  50. package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -1
  51. package/esm/drivers/mongodb/mongodb-id-generator.mjs +1 -1
  52. package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -1
  53. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -1
  54. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  55. package/esm/drivers/mongodb/mongodb-migration-driver.mjs +1 -1
  56. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  57. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +1 -1
  58. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  59. package/esm/drivers/mongodb/mongodb-query-builder.mjs +1 -1
  60. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  61. package/esm/drivers/mongodb/mongodb-query-operations.d.mts +1 -1
  62. package/esm/drivers/mongodb/mongodb-query-operations.d.mts.map +1 -1
  63. package/esm/drivers/mongodb/mongodb-query-operations.mjs +1 -1
  64. package/esm/drivers/mongodb/mongodb-query-operations.mjs.map +1 -1
  65. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +1 -1
  66. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  67. package/esm/drivers/mongodb/mongodb-query-parser.mjs +1 -1
  68. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  69. package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts +1 -1
  70. package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts.map +1 -1
  71. package/esm/drivers/mongodb/mongodb-sync-adapter.mjs +1 -1
  72. package/esm/drivers/mongodb/mongodb-sync-adapter.mjs.map +1 -1
  73. package/esm/drivers/mongodb/mongodb-update-translator.mjs +1 -1
  74. package/esm/drivers/mongodb/mongodb-update-translator.mjs.map +1 -1
  75. package/esm/drivers/mongodb/pipeline-stage-object.mjs +1 -1
  76. package/esm/drivers/mongodb/pipeline-stage-object.mjs.map +1 -1
  77. package/esm/drivers/mongodb/types.d.mts +1 -1
  78. package/esm/drivers/mongodb/types.d.mts.map +1 -1
  79. package/esm/drivers/postgres/naming.mjs +1 -1
  80. package/esm/drivers/postgres/naming.mjs.map +1 -1
  81. package/esm/drivers/postgres/postgres-blueprint.d.mts +1 -1
  82. package/esm/drivers/postgres/postgres-blueprint.d.mts.map +1 -1
  83. package/esm/drivers/postgres/postgres-blueprint.mjs +1 -1
  84. package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
  85. package/esm/drivers/postgres/postgres-dialect.d.mts +1 -1
  86. package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
  87. package/esm/drivers/postgres/postgres-dialect.mjs +1 -1
  88. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  89. package/esm/drivers/postgres/postgres-driver.d.mts +1 -1
  90. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  91. package/esm/drivers/postgres/postgres-driver.mjs +1 -1
  92. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  93. package/esm/drivers/postgres/postgres-migration-driver.d.mts +1 -1
  94. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  95. package/esm/drivers/postgres/postgres-migration-driver.mjs +1 -1
  96. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  97. package/esm/drivers/postgres/postgres-query-builder.d.mts +1 -1
  98. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  99. package/esm/drivers/postgres/postgres-query-builder.mjs +1 -1
  100. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  101. package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -1
  102. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  103. package/esm/drivers/postgres/postgres-query-parser.mjs +1 -2
  104. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  105. package/esm/drivers/postgres/postgres-sql-serializer.mjs +1 -1
  106. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  107. package/esm/drivers/postgres/postgres-ttl-purge.mjs +1 -1
  108. package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -1
  109. package/esm/drivers/postgres/postgres-type-converters.mjs +1 -1
  110. package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -1
  111. package/esm/drivers/postgres/postgres-update-validator.mjs +1 -1
  112. package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -1
  113. package/esm/drivers/postgres/types.d.mts +1 -1
  114. package/esm/drivers/postgres/types.d.mts.map +1 -1
  115. package/esm/drivers/sql/sql-dialect.contract.d.mts +1 -1
  116. package/esm/drivers/sql/sql-dialect.contract.d.mts.map +1 -1
  117. package/esm/drivers/sql/sql-types.d.mts +1 -1
  118. package/esm/drivers/sql/sql-types.d.mts.map +1 -1
  119. package/esm/errors/missing-data-source.error.d.mts +1 -1
  120. package/esm/errors/missing-data-source.error.d.mts.map +1 -1
  121. package/esm/errors/missing-data-source.error.mjs +1 -1
  122. package/esm/errors/missing-data-source.error.mjs.map +1 -1
  123. package/esm/errors/transaction-rollback.error.d.mts +1 -1
  124. package/esm/errors/transaction-rollback.error.d.mts.map +1 -1
  125. package/esm/errors/transaction-rollback.error.mjs +1 -1
  126. package/esm/errors/transaction-rollback.error.mjs.map +1 -1
  127. package/esm/errors/undefined-where-value.error.d.mts +1 -1
  128. package/esm/errors/undefined-where-value.error.d.mts.map +1 -1
  129. package/esm/errors/undefined-where-value.error.mjs +1 -1
  130. package/esm/errors/undefined-where-value.error.mjs.map +1 -1
  131. package/esm/errors/unsafe-filter.error.d.mts +1 -1
  132. package/esm/errors/unsafe-filter.error.d.mts.map +1 -1
  133. package/esm/errors/unsafe-filter.error.mjs +1 -1
  134. package/esm/errors/unsafe-filter.error.mjs.map +1 -1
  135. package/esm/errors/unsafe-raw-expression.error.d.mts +1 -1
  136. package/esm/errors/unsafe-raw-expression.error.d.mts.map +1 -1
  137. package/esm/errors/unsafe-raw-expression.error.mjs +1 -1
  138. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
  139. package/esm/errors/unsupported-lean-operation.error.d.mts +1 -1
  140. package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -1
  141. package/esm/errors/unsupported-lean-operation.error.mjs +1 -1
  142. package/esm/errors/unsupported-lean-operation.error.mjs.map +1 -1
  143. package/esm/errors/unsupported-lock-for-update.error.d.mts +1 -1
  144. package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -1
  145. package/esm/errors/unsupported-lock-for-update.error.mjs +1 -1
  146. package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -1
  147. package/esm/errors/unsupported-query-operation.error.d.mts +1 -1
  148. package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -1
  149. package/esm/errors/unsupported-query-operation.error.mjs +1 -1
  150. package/esm/errors/unsupported-query-operation.error.mjs.map +1 -1
  151. package/esm/errors/unsupported-update-operation.error.d.mts +1 -1
  152. package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -1
  153. package/esm/errors/unsupported-update-operation.error.mjs +1 -1
  154. package/esm/errors/unsupported-update-operation.error.mjs.map +1 -1
  155. package/esm/events/model-events.d.mts +1 -1
  156. package/esm/events/model-events.d.mts.map +1 -1
  157. package/esm/events/model-events.mjs +1 -1
  158. package/esm/events/model-events.mjs.map +1 -1
  159. package/esm/expressions/aggregate-expressions.d.mts +1 -1
  160. package/esm/expressions/aggregate-expressions.d.mts.map +1 -1
  161. package/esm/expressions/aggregate-expressions.mjs +1 -1
  162. package/esm/expressions/aggregate-expressions.mjs.map +1 -1
  163. package/esm/expressions/column-expressions.d.mts +1 -1
  164. package/esm/expressions/column-expressions.d.mts.map +1 -1
  165. package/esm/expressions/column-expressions.mjs +1 -1
  166. package/esm/expressions/column-expressions.mjs.map +1 -1
  167. package/esm/migration/column-builder.d.mts +1 -1
  168. package/esm/migration/column-builder.d.mts.map +1 -1
  169. package/esm/migration/column-builder.mjs +1 -1
  170. package/esm/migration/column-builder.mjs.map +1 -1
  171. package/esm/migration/column-helpers.d.mts +1 -1
  172. package/esm/migration/column-helpers.d.mts.map +1 -1
  173. package/esm/migration/column-helpers.mjs +1 -1
  174. package/esm/migration/column-helpers.mjs.map +1 -1
  175. package/esm/migration/foreign-key-builder.d.mts +1 -1
  176. package/esm/migration/foreign-key-builder.d.mts.map +1 -1
  177. package/esm/migration/foreign-key-builder.mjs +1 -1
  178. package/esm/migration/foreign-key-builder.mjs.map +1 -1
  179. package/esm/migration/migration-order.mjs +1 -1
  180. package/esm/migration/migration-order.mjs.map +1 -1
  181. package/esm/migration/migration-runner.d.mts +1 -1
  182. package/esm/migration/migration-runner.d.mts.map +1 -1
  183. package/esm/migration/migration-runner.mjs +1 -1
  184. package/esm/migration/migration-runner.mjs.map +1 -1
  185. package/esm/migration/migration.d.mts +1 -1
  186. package/esm/migration/migration.d.mts.map +1 -1
  187. package/esm/migration/migration.mjs +1 -1
  188. package/esm/migration/migration.mjs.map +1 -1
  189. package/esm/migration/parse-created-at.mjs +1 -1
  190. package/esm/migration/parse-created-at.mjs.map +1 -1
  191. package/esm/migration/sql-grammar.mjs +1 -1
  192. package/esm/migration/sql-grammar.mjs.map +1 -1
  193. package/esm/migration/sql-serializer.d.mts +1 -1
  194. package/esm/migration/sql-serializer.d.mts.map +1 -1
  195. package/esm/migration/sql-serializer.mjs +1 -1
  196. package/esm/migration/sql-serializer.mjs.map +1 -1
  197. package/esm/migration/types.d.mts +1 -1
  198. package/esm/migration/types.d.mts.map +1 -1
  199. package/esm/model/generate-model-id.mjs +1 -1
  200. package/esm/model/generate-model-id.mjs.map +1 -1
  201. package/esm/model/methods/accessor-methods.mjs +1 -1
  202. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  203. package/esm/model/methods/delete-methods.mjs +1 -1
  204. package/esm/model/methods/delete-methods.mjs.map +1 -1
  205. package/esm/model/methods/dirty-methods.mjs +1 -1
  206. package/esm/model/methods/dirty-methods.mjs.map +1 -1
  207. package/esm/model/methods/hydration-methods.mjs +1 -1
  208. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  209. package/esm/model/methods/instance-event-methods.mjs +1 -1
  210. package/esm/model/methods/instance-event-methods.mjs.map +1 -1
  211. package/esm/model/methods/meta-methods.mjs +1 -1
  212. package/esm/model/methods/meta-methods.mjs.map +1 -1
  213. package/esm/model/methods/pivot-methods.mjs +1 -1
  214. package/esm/model/methods/pivot-methods.mjs.map +1 -1
  215. package/esm/model/methods/query-methods.mjs +1 -1
  216. package/esm/model/methods/query-methods.mjs.map +1 -1
  217. package/esm/model/methods/restore-methods.mjs +1 -1
  218. package/esm/model/methods/restore-methods.mjs.map +1 -1
  219. package/esm/model/methods/scope-methods.mjs +1 -1
  220. package/esm/model/methods/scope-methods.mjs.map +1 -1
  221. package/esm/model/methods/serialization-methods.mjs +1 -1
  222. package/esm/model/methods/serialization-methods.mjs.map +1 -1
  223. package/esm/model/methods/static-event-methods.mjs +1 -1
  224. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  225. package/esm/model/methods/write-methods.d.mts +1 -1
  226. package/esm/model/methods/write-methods.d.mts.map +1 -1
  227. package/esm/model/methods/write-methods.mjs +1 -1
  228. package/esm/model/methods/write-methods.mjs.map +1 -1
  229. package/esm/model/model.d.mts +1 -1
  230. package/esm/model/model.d.mts.map +1 -1
  231. package/esm/model/model.mjs +1 -1
  232. package/esm/model/model.mjs.map +1 -1
  233. package/esm/model/model.types.d.mts +1 -1
  234. package/esm/model/model.types.d.mts.map +1 -1
  235. package/esm/model/register-model.d.mts +1 -1
  236. package/esm/model/register-model.d.mts.map +1 -1
  237. package/esm/model/register-model.mjs +1 -1
  238. package/esm/model/register-model.mjs.map +1 -1
  239. package/esm/model/relation-decorators.d.mts +1 -1
  240. package/esm/model/relation-decorators.d.mts.map +1 -1
  241. package/esm/model/relation-decorators.mjs +1 -1
  242. package/esm/model/relation-decorators.mjs.map +1 -1
  243. package/esm/operations/database.d.mts +1 -1
  244. package/esm/operations/database.d.mts.map +1 -1
  245. package/esm/operations/database.mjs +1 -1
  246. package/esm/operations/database.mjs.map +1 -1
  247. package/esm/operations/migrations.d.mts +1 -1
  248. package/esm/operations/migrations.d.mts.map +1 -1
  249. package/esm/operations/migrations.mjs +1 -1
  250. package/esm/operations/migrations.mjs.map +1 -1
  251. package/esm/query-builder/lean-records.mjs +1 -1
  252. package/esm/query-builder/lean-records.mjs.map +1 -1
  253. package/esm/query-builder/query-builder.d.mts +1 -1
  254. package/esm/query-builder/query-builder.d.mts.map +1 -1
  255. package/esm/query-builder/query-builder.mjs +1 -1
  256. package/esm/query-builder/query-builder.mjs.map +1 -1
  257. package/esm/relations/key-conventions.mjs +1 -1
  258. package/esm/relations/key-conventions.mjs.map +1 -1
  259. package/esm/relations/pivot-operations.d.mts +1 -1
  260. package/esm/relations/pivot-operations.d.mts.map +1 -1
  261. package/esm/relations/pivot-operations.mjs +1 -1
  262. package/esm/relations/pivot-operations.mjs.map +1 -1
  263. package/esm/relations/relation-hydrator.d.mts +1 -1
  264. package/esm/relations/relation-hydrator.d.mts.map +1 -1
  265. package/esm/relations/relation-hydrator.mjs +1 -1
  266. package/esm/relations/relation-hydrator.mjs.map +1 -1
  267. package/esm/relations/relation-loader.d.mts +1 -1
  268. package/esm/relations/relation-loader.d.mts.map +1 -1
  269. package/esm/relations/relation-loader.mjs +1 -1
  270. package/esm/relations/relation-loader.mjs.map +1 -1
  271. package/esm/relations/types.d.mts +1 -1
  272. package/esm/relations/types.d.mts.map +1 -1
  273. package/esm/remover/database-remover.d.mts +1 -1
  274. package/esm/remover/database-remover.d.mts.map +1 -1
  275. package/esm/remover/database-remover.mjs +1 -1
  276. package/esm/remover/database-remover.mjs.map +1 -1
  277. package/esm/restorer/database-restorer.d.mts +1 -1
  278. package/esm/restorer/database-restorer.d.mts.map +1 -1
  279. package/esm/restorer/database-restorer.mjs +1 -1
  280. package/esm/restorer/database-restorer.mjs.map +1 -1
  281. package/esm/sql-database-dirty-tracker.d.mts +1 -1
  282. package/esm/sql-database-dirty-tracker.d.mts.map +1 -1
  283. package/esm/sql-database-dirty-tracker.mjs +1 -1
  284. package/esm/sql-database-dirty-tracker.mjs.map +1 -1
  285. package/esm/sync/model-events.mjs +1 -1
  286. package/esm/sync/model-events.mjs.map +1 -1
  287. package/esm/sync/model-sync-operation.d.mts +1 -1
  288. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  289. package/esm/sync/model-sync-operation.mjs +1 -1
  290. package/esm/sync/model-sync-operation.mjs.map +1 -1
  291. package/esm/sync/model-sync.d.mts +1 -1
  292. package/esm/sync/model-sync.d.mts.map +1 -1
  293. package/esm/sync/model-sync.mjs +1 -1
  294. package/esm/sync/model-sync.mjs.map +1 -1
  295. package/esm/sync/sync-context.d.mts +1 -1
  296. package/esm/sync/sync-context.d.mts.map +1 -1
  297. package/esm/sync/sync-context.mjs +1 -1
  298. package/esm/sync/sync-context.mjs.map +1 -1
  299. package/esm/sync/sync-fanout.mjs +1 -1
  300. package/esm/sync/sync-fanout.mjs.map +1 -1
  301. package/esm/sync/sync-manager.d.mts +1 -1
  302. package/esm/sync/sync-manager.d.mts.map +1 -1
  303. package/esm/sync/sync-manager.mjs +1 -1
  304. package/esm/sync/sync-manager.mjs.map +1 -1
  305. package/esm/sync/types.d.mts +1 -1
  306. package/esm/sync/types.d.mts.map +1 -1
  307. package/esm/transactions/after-commit.d.mts +1 -1
  308. package/esm/transactions/after-commit.d.mts.map +1 -1
  309. package/esm/transactions/after-commit.mjs +1 -1
  310. package/esm/transactions/after-commit.mjs.map +1 -1
  311. package/esm/types.d.mts +1 -1
  312. package/esm/types.d.mts.map +1 -1
  313. package/esm/utils/connect-to-database.d.mts +1 -1
  314. package/esm/utils/connect-to-database.d.mts.map +1 -1
  315. package/esm/utils/connect-to-database.mjs +1 -1
  316. package/esm/utils/connect-to-database.mjs.map +1 -1
  317. package/esm/utils/database-writer.utils.d.mts +1 -1
  318. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  319. package/esm/utils/database-writer.utils.mjs +1 -1
  320. package/esm/utils/database-writer.utils.mjs.map +1 -1
  321. package/esm/utils/define-model.d.mts +1 -1
  322. package/esm/utils/define-model.d.mts.map +1 -1
  323. package/esm/utils/define-model.mjs +1 -1
  324. package/esm/utils/define-model.mjs.map +1 -1
  325. package/esm/utils/escape-regex.d.mts +1 -1
  326. package/esm/utils/escape-regex.d.mts.map +1 -1
  327. package/esm/utils/escape-regex.mjs +1 -1
  328. package/esm/utils/escape-regex.mjs.map +1 -1
  329. package/esm/utils/is-valid-date-value.mjs +1 -1
  330. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  331. package/esm/utils/once-connected.d.mts +1 -1
  332. package/esm/utils/once-connected.d.mts.map +1 -1
  333. package/esm/utils/once-connected.mjs +1 -1
  334. package/esm/utils/once-connected.mjs.map +1 -1
  335. package/esm/utils/prune-empty-parents.mjs +1 -1
  336. package/esm/utils/prune-empty-parents.mjs.map +1 -1
  337. package/esm/utils/sanitize-filter.d.mts +1 -1
  338. package/esm/utils/sanitize-filter.d.mts.map +1 -1
  339. package/esm/utils/sanitize-filter.mjs +1 -1
  340. package/esm/utils/sanitize-filter.mjs.map +1 -1
  341. package/esm/validation/database-seal-plugins.mjs +1 -1
  342. package/esm/validation/database-seal-plugins.mjs.map +1 -1
  343. package/esm/validation/database-writer-validation-error.d.mts +1 -1
  344. package/esm/validation/database-writer-validation-error.d.mts.map +1 -1
  345. package/esm/validation/database-writer-validation-error.mjs +1 -1
  346. package/esm/validation/database-writer-validation-error.mjs.map +1 -1
  347. package/esm/validation/mutators/embed-mutator.mjs +1 -1
  348. package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
  349. package/esm/validation/plugins/database-rules-plugin.d.mts +1 -1
  350. package/esm/validation/plugins/database-rules-plugin.d.mts.map +1 -1
  351. package/esm/validation/plugins/database-rules-plugin.mjs +1 -1
  352. package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
  353. package/esm/validation/plugins/embed-validator-plugin.d.mts +1 -1
  354. package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
  355. package/esm/validation/plugins/embed-validator-plugin.mjs +1 -1
  356. package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
  357. package/esm/validation/rules/database-model-rule.mjs +1 -1
  358. package/esm/validation/rules/database-model-rule.mjs.map +1 -1
  359. package/esm/validation/rules/exists-rule.mjs +1 -1
  360. package/esm/validation/rules/exists-rule.mjs.map +1 -1
  361. package/esm/validation/rules/unique-rule.mjs +1 -1
  362. package/esm/validation/rules/unique-rule.mjs.map +1 -1
  363. package/esm/validation/transformers/embed-model-transformer.mjs +1 -1
  364. package/esm/validation/transformers/embed-model-transformer.mjs.map +1 -1
  365. package/esm/validation/types.d.mts +1 -1
  366. package/esm/validation/types.d.mts.map +1 -1
  367. package/esm/validation/validators/embed-validator.d.mts +1 -1
  368. package/esm/validation/validators/embed-validator.d.mts.map +1 -1
  369. package/esm/validation/validators/embed-validator.mjs +1 -1
  370. package/esm/validation/validators/embed-validator.mjs.map +1 -1
  371. package/esm/writer/database-writer.d.mts +1 -1
  372. package/esm/writer/database-writer.d.mts.map +1 -1
  373. package/esm/writer/database-writer.mjs +1 -1
  374. package/esm/writer/database-writer.mjs.map +1 -1
  375. package/llms-full.txt +541 -434
  376. package/package.json +7 -6
  377. package/skills/aggregate-data/SKILL.md +28 -17
  378. package/skills/alter-migration/SKILL.md +325 -233
  379. package/skills/cascade-basics/SKILL.md +19 -19
  380. package/skills/configure-delete-strategy/SKILL.md +11 -11
  381. package/skills/define-model/SKILL.md +19 -17
  382. package/skills/define-relations/SKILL.md +13 -18
  383. package/skills/index.md +3 -0
  384. package/skills/manage-transactions/SKILL.md +18 -12
  385. package/skills/paginate-results/SKILL.md +6 -8
  386. package/skills/perform-atomic-ops/SKILL.md +26 -21
  387. package/skills/query-data/SKILL.md +27 -33
  388. package/skills/run-cascade-cli/SKILL.md +11 -10
  389. package/skills/search-by-vector/SKILL.md +8 -5
  390. package/skills/subscribe-to-model-events/SKILL.md +17 -17
  391. package/skills/track-changes/SKILL.md +9 -9
  392. package/skills/write-migration/SKILL.md +4 -4
package/llms-full.txt CHANGED
@@ -18,23 +18,21 @@ Cascade's query builder isn't only for finding records — it crunches numbers t
18
18
  ## Scalar aggregates
19
19
 
20
20
  ```ts
21
- const total = await Order.count(); // count is static on the model
22
- const revenue = await Order.query().sum("total");
23
- const avgTicket = await Order.query().avg("total");
24
- const cheapest = await Order.query().min("total");
25
- const priciest = await Order.query().max("total");
21
+ const total = await Order.count(); // count is static on the model
22
+ const revenue = await Order.query().sum("total");
23
+ const avgTicket = await Order.query().avg("total");
24
+ const cheapest = await Order.query().min("total");
25
+ const priciest = await Order.query().max("total");
26
26
 
27
27
  // Filtered
28
- const monthRevenue = await Order.query()
29
- .whereDateAfter("created_at", startOfMonth)
30
- .sum("total");
28
+ const monthRevenue = await Order.query().whereDateAfter("created_at", startOfMonth).sum("total");
31
29
  ```
32
30
 
33
31
  Each returns a single number (`Promise<number>`). Only `count` is a static shortcut on the model; `sum` / `avg` / `min` / `max` (and the date helpers) are query-builder methods — reach them via `Order.query()` or chain off `Order.where(...)`. Date filters take a single date: `whereDate(field, value)` (exact day, time ignored), `whereDateAfter` / `whereDateBefore` (one-sided), `whereDateBetween(field, [a, b])` (range) — there is no 3-arg `whereDate(field, op, value)`. By Cascade convention the scalar terminators return **`0` on zero rows — not `null`** — so you can use the result directly without a null guard.
34
32
 
35
33
  ## Grouped rollups — two-arg `groupBy(fields, aggregates)`
36
34
 
37
- To compute aggregates *per group*, pass a second argument to `groupBy`: an object mapping output aliases to aggregate expressions. Build the expressions with the `$agg` helpers:
35
+ To compute aggregates _per group_, pass a second argument to `groupBy`: an object mapping output aliases to aggregate expressions. Build the expressions with the `$agg` helpers:
38
36
 
39
37
  ```ts
40
38
  import { $agg } from "@warlock.js/cascade";
@@ -67,8 +65,8 @@ import { $agg, $expr } from "@warlock.js/cascade";
67
65
 
68
66
  const revenue = await OrderItem.query()
69
67
  .groupBy("product_id", {
70
- revenue: $agg.sum($expr.mul("price", "quantity")), // SUM(price * quantity)
71
- net: $agg.sum($expr.mul($expr.sub($expr.lit(1), "discount"), "price")),// SUM((1 - discount) * price)
68
+ revenue: $agg.sum($expr.mul("price", "quantity")), // SUM(price * quantity)
69
+ net: $agg.sum($expr.mul($expr.sub($expr.lit(1), "discount"), "price")), // SUM((1 - discount) * price)
72
70
  })
73
71
  .get();
74
72
  ```
@@ -116,7 +114,7 @@ const monthly = await Order.query()
116
114
 
117
115
  ## `.having(...)` — filter groups by a computed aggregate
118
116
 
119
- `.where()` filters rows *before* grouping (cheap, uses indexes). `.having()` filters *after* aggregation, by the alias you defined:
117
+ `.where()` filters rows _before_ grouping (cheap, uses indexes). `.having()` filters _after_ aggregation, by the alias you defined:
120
118
 
121
119
  ```ts
122
120
  const big = await Order.query()
@@ -145,14 +143,27 @@ The `orderBy` reference matches the alias from the aggregates object.
145
143
  const rows = await Post.query().unwind("tags").where("tags", "news").lean().get();
146
144
 
147
145
  // Keep posts with no tags; record each tag's position
148
- await Post.query().unwind("tags", { preserveNullAndEmptyArrays: true, includeArrayIndex: "position" }).get();
146
+ await Post.query()
147
+ .unwind("tags", { preserveNullAndEmptyArrays: true, includeArrayIndex: "position" })
148
+ .get();
149
149
 
150
150
  // Computed field, existing fields kept
151
- await Post.query().addFields({ score: { $add: ["$likes", "$shares"] } }).orderBy("score", "desc").get();
151
+ await Post.query()
152
+ .addFields({ score: { $add: ["$likes", "$shares"] } })
153
+ .orderBy("score", "desc")
154
+ .get();
152
155
 
153
156
  // Pipeline $lookup
154
- await Post.query().join({ table: "comments", localField: "id", foreignField: "postId",
155
- alias: "approved", pipeline: [{ $match: { approved: true } }] }).lean().get();
157
+ await Post.query()
158
+ .join({
159
+ table: "comments",
160
+ localField: "id",
161
+ foreignField: "postId",
162
+ alias: "approved",
163
+ pipeline: [{ $match: { approved: true } }],
164
+ })
165
+ .lean()
166
+ .get();
156
167
  ```
157
168
 
158
169
  `unwind()` returns several rows for one stored document. Use `.lean()` when you only read them. `addFields` expressions are raw aggregation expressions: write them in code, never from request data. Computed columns in the output also work with `selectRaw({ alias: expr })` (`$project`). There is no `$facet` method yet.
@@ -164,7 +175,7 @@ Raw stages: `joinRaw({ $lookup: { from, let, pipeline, as } })` (or an array of
164
175
  ## Gotchas
165
176
 
166
177
  - **`where` vs `having`.** Row filters go in `.where()` (before grouping, index-friendly); aggregate filters go in `.having()`.
167
- - **Empty sets.** Scalar terminators return `0` on zero rows (above). For *grouped* reports, a group with no rows simply doesn't appear — a report over an empty range returns `[]`, not a row of zeros.
178
+ - **Empty sets.** Scalar terminators return `0` on zero rows (above). For _grouped_ reports, a group with no rows simply doesn't appear — a report over an empty range returns `[]`, not a row of zeros.
168
179
  - **Don't `.all()` then `array.reduce`.** Always push aggregates to the database.
169
180
 
170
181
  ## See also
@@ -194,15 +205,19 @@ Use `Migration.alter` to evolve an existing table — adding or dropping columns
194
205
  import { Migration, text } from "@warlock.js/cascade";
195
206
  import { User } from "../user.model";
196
207
 
197
- export default Migration.alter(User, {
198
- add: {
199
- phone: text().nullable(),
208
+ export default Migration.alter(
209
+ User,
210
+ {
211
+ add: {
212
+ phone: text().nullable(),
213
+ },
200
214
  },
201
- }, {
202
- down() {
203
- this.dropColumn("phone");
215
+ {
216
+ down() {
217
+ this.dropColumn("phone");
218
+ },
204
219
  },
205
- });
220
+ );
206
221
  ```
207
222
 
208
223
  ---
@@ -232,21 +247,25 @@ import { Migration, text, integer, bool, uuid } from "@warlock.js/cascade";
232
247
  import { Organization } from "app/organizations/models/organization";
233
248
  import { User } from "../user.model";
234
249
 
235
- export default Migration.alter(User, {
236
- add: {
237
- phone: text().nullable(),
238
- login_attempts: integer().notNullable().default(0),
239
- is_verified: bool().notNullable().default(false),
240
- organization_id: uuid().references(Organization.table).onDelete("cascade").notNullable(),
250
+ export default Migration.alter(
251
+ User,
252
+ {
253
+ add: {
254
+ phone: text().nullable(),
255
+ login_attempts: integer().notNullable().default(0),
256
+ is_verified: bool().notNullable().default(false),
257
+ organization_id: uuid().references(Organization.table).onDelete("cascade").notNullable(),
258
+ },
241
259
  },
242
- }, {
243
- down() {
244
- this.dropColumn("phone");
245
- this.dropColumn("login_attempts");
246
- this.dropColumn("is_verified");
247
- this.dropColumn("organization_id");
260
+ {
261
+ down() {
262
+ this.dropColumn("phone");
263
+ this.dropColumn("login_attempts");
264
+ this.dropColumn("is_verified");
265
+ this.dropColumn("organization_id");
266
+ },
248
267
  },
249
- });
268
+ );
250
269
  ```
251
270
 
252
271
  Column helpers (`text`, `integer`, `bool`, `uuid`, `double`, `json`, `timestamp`, `date`, …) are imported from `@warlock.js/cascade` and support the same modifier chain as in `Migration.create`.
@@ -254,31 +273,39 @@ Column helpers (`text`, `integer`, `bool`, `uuid`, `double`, `json`, `timestamp`
254
273
  ### `drop` — remove columns
255
274
 
256
275
  ```ts
257
- export default Migration.alter(User, {
258
- drop: ["legacy_token", "old_avatar_url"],
259
- }, {
260
- down() {
261
- // re-add with the original definition if rollback is meaningful
262
- this.text("legacy_token").nullable();
263
- this.text("old_avatar_url").nullable();
276
+ export default Migration.alter(
277
+ User,
278
+ {
279
+ drop: ["legacy_token", "old_avatar_url"],
264
280
  },
265
- });
281
+ {
282
+ down() {
283
+ // re-add with the original definition if rollback is meaningful
284
+ this.text("legacy_token").nullable();
285
+ this.text("old_avatar_url").nullable();
286
+ },
287
+ },
288
+ );
266
289
  ```
267
290
 
268
291
  ### `rename` — rename columns
269
292
 
270
293
  ```ts
271
- export default Migration.alter(Post, {
272
- rename: {
273
- body: "content",
274
- thumb_url: "thumbnail_url",
294
+ export default Migration.alter(
295
+ Post,
296
+ {
297
+ rename: {
298
+ body: "content",
299
+ thumb_url: "thumbnail_url",
300
+ },
275
301
  },
276
- }, {
277
- down() {
278
- this.renameColumn("content", "body");
279
- this.renameColumn("thumbnail_url", "thumb_url");
302
+ {
303
+ down() {
304
+ this.renameColumn("content", "body");
305
+ this.renameColumn("thumbnail_url", "thumb_url");
306
+ },
280
307
  },
281
- });
308
+ );
282
309
  ```
283
310
 
284
311
  ### `modify` — change column type or nullability
@@ -287,17 +314,21 @@ export default Migration.alter(Post, {
287
314
  import { Migration, string } from "@warlock.js/cascade";
288
315
  import { User } from "../user.model";
289
316
 
290
- export default Migration.alter(User, {
291
- modify: {
292
- // widen email from text to varchar(320) and keep not-null
293
- email: string(320).notNullable(),
317
+ export default Migration.alter(
318
+ User,
319
+ {
320
+ modify: {
321
+ // widen email from text to varchar(320) and keep not-null
322
+ email: string(320).notNullable(),
323
+ },
294
324
  },
295
- }, {
296
- down() {
297
- // revert to unbounded text
298
- this.text("email").notNullable();
325
+ {
326
+ down() {
327
+ // revert to unbounded text
328
+ this.text("email").notNullable();
329
+ },
299
330
  },
300
- });
331
+ );
301
332
  ```
302
333
 
303
334
  > `modify` re-declares the column. Include every modifier you want to keep (`.notNullable()`, `.default(…)`, etc.) — the declaration replaces the old one entirely.
@@ -309,29 +340,37 @@ export default Migration.alter(User, {
309
340
  ### Regular indexes — `addIndex` / `dropIndex`
310
341
 
311
342
  ```ts
312
- export default Migration.alter(Order, {
313
- // single column
314
- addIndex: [{ columns: "status" }],
315
- }, {
316
- down() {
317
- this.dropIndex("status");
343
+ export default Migration.alter(
344
+ Order,
345
+ {
346
+ // single column
347
+ addIndex: [{ columns: "status" }],
318
348
  },
319
- });
349
+ {
350
+ down() {
351
+ this.dropIndex("status");
352
+ },
353
+ },
354
+ );
320
355
  ```
321
356
 
322
357
  ```ts
323
358
  // composite index with an explicit name
324
- export default Migration.alter(Order, {
325
- addIndex: [
326
- { columns: ["organization_id", "status"], name: "idx_org_status" },
327
- { columns: "created_at" },
328
- ],
329
- }, {
330
- down() {
331
- this.dropIndex("idx_org_status");
332
- this.dropIndex("created_at");
359
+ export default Migration.alter(
360
+ Order,
361
+ {
362
+ addIndex: [
363
+ { columns: ["organization_id", "status"], name: "idx_org_status" },
364
+ { columns: "created_at" },
365
+ ],
333
366
  },
334
- });
367
+ {
368
+ down() {
369
+ this.dropIndex("idx_org_status");
370
+ this.dropIndex("created_at");
371
+ },
372
+ },
373
+ );
335
374
  ```
336
375
 
337
376
  `dropIndex` accepts a name string or a columns array:
@@ -345,63 +384,79 @@ export default Migration.alter(Order, {
345
384
  `options.concurrently: true` builds the index without locking the table (Postgres):
346
385
 
347
386
  ```ts
348
- addIndex: [{ columns: "email", options: { concurrently: true } }]
387
+ addIndex: [{ columns: "email", options: { concurrently: true } }];
349
388
  ```
350
389
 
351
390
  ### Unique constraints — `addUnique` / `dropUnique`
352
391
 
353
392
  ```ts
354
393
  // promote an indexed column to unique
355
- export default Migration.alter(User, {
356
- addUnique: [{ columns: "email" }],
357
- }, {
358
- down() {
359
- this.dropUnique("email");
394
+ export default Migration.alter(
395
+ User,
396
+ {
397
+ addUnique: [{ columns: "email" }],
360
398
  },
361
- });
399
+ {
400
+ down() {
401
+ this.dropUnique("email");
402
+ },
403
+ },
404
+ );
362
405
  ```
363
406
 
364
407
  Composite unique constraint:
365
408
 
366
409
  ```ts
367
- export default Migration.alter(Membership, {
368
- addUnique: [{ columns: ["organization_id", "user_id"], name: "uq_org_member" }],
369
- }, {
370
- down() {
371
- this.dropUnique(["organization_id", "user_id"]);
410
+ export default Migration.alter(
411
+ Membership,
412
+ {
413
+ addUnique: [{ columns: ["organization_id", "user_id"], name: "uq_org_member" }],
372
414
  },
373
- });
415
+ {
416
+ down() {
417
+ this.dropUnique(["organization_id", "user_id"]);
418
+ },
419
+ },
420
+ );
374
421
  ```
375
422
 
376
423
  Downgrade a unique to a plain index (the pattern used in this session):
377
424
 
378
425
  ```ts
379
- export default Migration.alter(User, {
380
- dropUnique: [["email"]],
381
- addIndex: [{ columns: "email", name: "users_email_index" }],
382
- }, {
383
- down() {
384
- this.dropIndex("users_email_index");
385
- this.unique("email");
426
+ export default Migration.alter(
427
+ User,
428
+ {
429
+ dropUnique: [["email"]],
430
+ addIndex: [{ columns: "email", name: "users_email_index" }],
386
431
  },
387
- });
432
+ {
433
+ down() {
434
+ this.dropIndex("users_email_index");
435
+ this.unique("email");
436
+ },
437
+ },
438
+ );
388
439
  ```
389
440
 
390
441
  ### Expression indexes — `addExpressionIndex` (Postgres)
391
442
 
392
443
  ```ts
393
- export default Migration.alter(User, {
394
- addExpressionIndex: [
395
- { expressions: "lower(email)", name: "idx_email_ci" },
396
- { expressions: "lower(name)", name: "idx_name_ci" },
397
- { expressions: ["year(created_at)", "status"] },
398
- ],
399
- }, {
400
- down() {
401
- this.dropIndex("idx_email_ci");
402
- this.dropIndex("idx_name_ci");
444
+ export default Migration.alter(
445
+ User,
446
+ {
447
+ addExpressionIndex: [
448
+ { expressions: "lower(email)", name: "idx_email_ci" },
449
+ { expressions: "lower(name)", name: "idx_name_ci" },
450
+ { expressions: ["year(created_at)", "status"] },
451
+ ],
403
452
  },
404
- });
453
+ {
454
+ down() {
455
+ this.dropIndex("idx_email_ci");
456
+ this.dropIndex("idx_name_ci");
457
+ },
458
+ },
459
+ );
405
460
  ```
406
461
 
407
462
  ---
@@ -411,30 +466,38 @@ export default Migration.alter(User, {
411
466
  ### Full-text — `addFullText` / `dropFullText`
412
467
 
413
468
  ```ts
414
- export default Migration.alter(Article, {
415
- addFullText: [
416
- {
417
- columns: ["title", "body"],
418
- options: { name: "ft_article_search", language: "english" },
469
+ export default Migration.alter(
470
+ Article,
471
+ {
472
+ addFullText: [
473
+ {
474
+ columns: ["title", "body"],
475
+ options: { name: "ft_article_search", language: "english" },
476
+ },
477
+ ],
478
+ },
479
+ {
480
+ down() {
481
+ this.dropFullText("ft_article_search");
419
482
  },
420
- ],
421
- }, {
422
- down() {
423
- this.dropFullText("ft_article_search");
424
483
  },
425
- });
484
+ );
426
485
  ```
427
486
 
428
487
  ### Geo-spatial — `addGeoIndex` / `dropGeoIndex`
429
488
 
430
489
  ```ts
431
- export default Migration.alter(Store, {
432
- addGeoIndex: [{ column: "location" }],
433
- }, {
434
- down() {
435
- this.dropGeoIndex("location");
490
+ export default Migration.alter(
491
+ Store,
492
+ {
493
+ addGeoIndex: [{ column: "location" }],
436
494
  },
437
- });
495
+ {
496
+ down() {
497
+ this.dropGeoIndex("location");
498
+ },
499
+ },
500
+ );
438
501
  ```
439
502
 
440
503
  Options: `{ name?, type?: "2dsphere" | "2d", min?, max? }`
@@ -442,16 +505,22 @@ Options: `{ name?, type?: "2dsphere" | "2d", min?, max? }`
442
505
  ### Vector (AI embeddings) — `addVectorIndex` / `dropVectorIndex`
443
506
 
444
507
  ```ts
445
- export default Migration.alter(Document, {
446
- addVectorIndex: [{
447
- column: "embedding",
448
- options: { dimensions: 1536, similarity: "cosine", name: "idx_doc_embedding" },
449
- }],
450
- }, {
451
- down() {
452
- this.dropVectorIndex("embedding");
508
+ export default Migration.alter(
509
+ Document,
510
+ {
511
+ addVectorIndex: [
512
+ {
513
+ column: "embedding",
514
+ options: { dimensions: 1536, similarity: "cosine", name: "idx_doc_embedding" },
515
+ },
516
+ ],
453
517
  },
454
- });
518
+ {
519
+ down() {
520
+ this.dropVectorIndex("embedding");
521
+ },
522
+ },
523
+ );
455
524
  ```
456
525
 
457
526
  Similarity options: `"cosine"` | `"euclidean"` | `"dotProduct"`.
@@ -461,13 +530,17 @@ Similarity options: `"cosine"` | `"euclidean"` | `"dotProduct"`.
461
530
  Expires documents automatically after a fixed duration:
462
531
 
463
532
  ```ts
464
- export default Migration.alter(Session, {
465
- addTTLIndex: [{ column: "created_at", expireAfterSeconds: 86400 }], // 24 h
466
- }, {
467
- down() {
468
- this.dropTTLIndex("created_at");
533
+ export default Migration.alter(
534
+ Session,
535
+ {
536
+ addTTLIndex: [{ column: "created_at", expireAfterSeconds: 86400 }], // 24 h
469
537
  },
470
- });
538
+ {
539
+ down() {
540
+ this.dropTTLIndex("created_at");
541
+ },
542
+ },
543
+ );
471
544
  ```
472
545
 
473
546
  ---
@@ -483,17 +556,21 @@ import { Migration } from "@warlock.js/cascade";
483
556
  import { Team } from "app/teams/models/team";
484
557
  import { User } from "../user.model";
485
558
 
486
- export default Migration.alter(User, {
487
- addForeign: [
488
- { column: "team_id", references: Team, onDelete: "cascade" },
489
- { column: "owner_id", references: "users", on: "id", onDelete: "setNull" },
490
- ],
491
- }, {
492
- down() {
493
- this.dropForeign("team_id", Team.table);
494
- this.dropForeign("owner_id", "users");
559
+ export default Migration.alter(
560
+ User,
561
+ {
562
+ addForeign: [
563
+ { column: "team_id", references: Team, onDelete: "cascade" },
564
+ { column: "owner_id", references: "users", on: "id", onDelete: "setNull" },
565
+ ],
495
566
  },
496
- });
567
+ {
568
+ down() {
569
+ this.dropForeign("team_id", Team.table);
570
+ this.dropForeign("owner_id", "users");
571
+ },
572
+ },
573
+ );
497
574
  ```
498
575
 
499
576
  `onDelete` / `onUpdate` values: `"cascade"` | `"restrict"` | `"setNull"` | `"noAction"`. Both default to `"restrict"` when omitted.
@@ -501,23 +578,27 @@ export default Migration.alter(User, {
501
578
  Drop by constraint name when you named it manually:
502
579
 
503
580
  ```ts
504
- dropForeign: [{ columnOrConstraint: "fk_my_custom_name" }]
581
+ dropForeign: [{ columnOrConstraint: "fk_my_custom_name" }];
505
582
  ```
506
583
 
507
584
  ### CHECK constraints — `addCheck` / `dropCheck`
508
585
 
509
586
  ```ts
510
- export default Migration.alter(Product, {
511
- addCheck: [
512
- { name: "price_positive", expression: "price_cents >= 0" },
513
- { name: "stock_non_negative", expression: "stock >= 0" },
514
- ],
515
- }, {
516
- down() {
517
- this.dropCheck("price_positive");
518
- this.dropCheck("stock_non_negative");
587
+ export default Migration.alter(
588
+ Product,
589
+ {
590
+ addCheck: [
591
+ { name: "price_positive", expression: "price_cents >= 0" },
592
+ { name: "stock_non_negative", expression: "stock >= 0" },
593
+ ],
519
594
  },
520
- });
595
+ {
596
+ down() {
597
+ this.dropCheck("price_positive");
598
+ this.dropCheck("stock_non_negative");
599
+ },
600
+ },
601
+ );
521
602
  ```
522
603
 
523
604
  ---
@@ -527,16 +608,20 @@ export default Migration.alter(Product, {
527
608
  Use `raw` when you need a one-off SQL statement alongside declarative changes (e.g. backfilling a new column in the same migration when the table is small):
528
609
 
529
610
  ```ts
530
- export default Migration.alter(User, {
531
- add: {
532
- full_name: text().nullable(),
611
+ export default Migration.alter(
612
+ User,
613
+ {
614
+ add: {
615
+ full_name: text().nullable(),
616
+ },
617
+ raw: `UPDATE users SET full_name = first_name || ' ' || last_name`,
533
618
  },
534
- raw: `UPDATE users SET full_name = first_name || ' ' || last_name`,
535
- }, {
536
- down() {
537
- this.dropColumn("full_name");
619
+ {
620
+ down() {
621
+ this.dropColumn("full_name");
622
+ },
538
623
  },
539
- });
624
+ );
540
625
  ```
541
626
 
542
627
  Pass an array for multiple statements:
@@ -545,7 +630,7 @@ Pass an array for multiple statements:
545
630
  raw: [
546
631
  `UPDATE users SET status = 'active' WHERE status IS NULL`,
547
632
  `UPDATE users SET role = 'member' WHERE role IS NULL`,
548
- ]
633
+ ];
549
634
  ```
550
635
 
551
636
  ---
@@ -554,24 +639,24 @@ raw: [
554
639
 
555
640
  `Migration.alter` **never** auto-infers rollback. Supply `down()` in options whenever you want `cascade migrate:rollback` to work. Inside `down()`, `this` is a full `Migration` instance — every class-form builder is available:
556
641
 
557
- | Operation | Class-form call in `down()` |
558
- |---|---|
559
- | Drop an added column | `this.dropColumn("name")` |
560
- | Re-add a dropped column | `this.text("name").nullable()` |
561
- | Undo a rename | `this.renameColumn("new", "old")` |
562
- | Undo a column modify | `this.text("col").notNullable()` |
563
- | Drop a plain index (by name) | `this.dropIndex("idx_name")` |
564
- | Drop a plain index (by columns) | `this.dropIndex(["col_a", "col_b"])` |
565
- | Drop a unique constraint | `this.dropUnique("email")` or `this.dropUnique(["a", "b"])` |
566
- | Add a unique constraint back | `this.unique("email")` |
567
- | Add a plain index back | `this.index("email", "idx_name")` |
568
- | Drop a foreign key | `this.dropForeign("col", "ref_table")` |
569
- | Drop a CHECK constraint | `this.dropCheck("constraint_name")` |
570
- | Drop full-text index | `this.dropFullText("ft_name")` |
571
- | Drop geo index | `this.dropGeoIndex("column")` |
572
- | Drop vector index | `this.dropVectorIndex("column")` |
573
- | Drop TTL index | `this.dropTTLIndex("column")` |
574
- | Raw SQL | `this.raw("SQL statement")` |
642
+ | Operation | Class-form call in `down()` |
643
+ | ------------------------------- | ----------------------------------------------------------- |
644
+ | Drop an added column | `this.dropColumn("name")` |
645
+ | Re-add a dropped column | `this.text("name").nullable()` |
646
+ | Undo a rename | `this.renameColumn("new", "old")` |
647
+ | Undo a column modify | `this.text("col").notNullable()` |
648
+ | Drop a plain index (by name) | `this.dropIndex("idx_name")` |
649
+ | Drop a plain index (by columns) | `this.dropIndex(["col_a", "col_b"])` |
650
+ | Drop a unique constraint | `this.dropUnique("email")` or `this.dropUnique(["a", "b"])` |
651
+ | Add a unique constraint back | `this.unique("email")` |
652
+ | Add a plain index back | `this.index("email", "idx_name")` |
653
+ | Drop a foreign key | `this.dropForeign("col", "ref_table")` |
654
+ | Drop a CHECK constraint | `this.dropCheck("constraint_name")` |
655
+ | Drop full-text index | `this.dropFullText("ft_name")` |
656
+ | Drop geo index | `this.dropGeoIndex("column")` |
657
+ | Drop vector index | `this.dropVectorIndex("column")` |
658
+ | Drop TTL index | `this.dropTTLIndex("column")` |
659
+ | Raw SQL | `this.raw("SQL statement")` |
575
660
 
576
661
  If a migration is genuinely irreversible, throw inside `down()` so accidental rollbacks fail loudly:
577
662
 
@@ -587,28 +672,32 @@ If a migration is genuinely irreversible, throw inside `down()` so accidental ro
587
672
 
588
673
  ## Options (`MigrationAlterOptions`)
589
674
 
590
- | Key | Type | Purpose |
591
- |---|---|---|
592
- | `down` | `(this: Migration) => void \| Promise<void>` | Rollback logic (not auto-inferred) |
593
- | `up` | `(this: Migration) => void \| Promise<void>` | Extra imperative logic to run after declarative changes |
594
- | `order` | `number` | Execution order override (default `0`) |
595
- | `createdAt` | `string` | ISO timestamp override for migration name ordering |
596
- | `transactional` | `boolean` | Wrap the migration in a DB transaction |
675
+ | Key | Type | Purpose |
676
+ | --------------- | -------------------------------------------- | ------------------------------------------------------- |
677
+ | `down` | `(this: Migration) => void \| Promise<void>` | Rollback logic (not auto-inferred) |
678
+ | `up` | `(this: Migration) => void \| Promise<void>` | Extra imperative logic to run after declarative changes |
679
+ | `order` | `number` | Execution order override (default `0`) |
680
+ | `createdAt` | `string` | ISO timestamp override for migration name ordering |
681
+ | `transactional` | `boolean` | Wrap the migration in a DB transaction |
597
682
 
598
683
  The `up` hook runs **after** the declarative schema changes, making it useful for data backfills or conditional DDL that depends on the added schema:
599
684
 
600
685
  ```ts
601
- export default Migration.alter(User, {
602
- add: { verified_at: timestamp().nullable() },
603
- }, {
604
- up() {
605
- // backfill: mark existing confirmed users as verified
606
- this.raw(`UPDATE users SET verified_at = updated_at WHERE status = 'confirmed'`);
686
+ export default Migration.alter(
687
+ User,
688
+ {
689
+ add: { verified_at: timestamp().nullable() },
607
690
  },
608
- down() {
609
- this.dropColumn("verified_at");
691
+ {
692
+ up() {
693
+ // backfill: mark existing confirmed users as verified
694
+ this.raw(`UPDATE users SET verified_at = updated_at WHERE status = 'confirmed'`);
695
+ },
696
+ down() {
697
+ this.dropColumn("verified_at");
698
+ },
610
699
  },
611
- });
700
+ );
612
701
  ```
613
702
 
614
703
  ---
@@ -622,48 +711,58 @@ import { Migration, uuid } from "@warlock.js/cascade";
622
711
  import { Team } from "app/teams/models/team";
623
712
  import { User } from "../user.model";
624
713
 
625
- export default Migration.alter(User, {
626
- add: {
627
- team_id: uuid().nullable(),
714
+ export default Migration.alter(
715
+ User,
716
+ {
717
+ add: {
718
+ team_id: uuid().nullable(),
719
+ },
720
+ addForeign: [{ column: "team_id", references: Team, onDelete: "setNull" }],
721
+ addIndex: [{ columns: "team_id" }],
628
722
  },
629
- addForeign: [
630
- { column: "team_id", references: Team, onDelete: "setNull" },
631
- ],
632
- addIndex: [{ columns: "team_id" }],
633
- }, {
634
- down() {
635
- this.dropForeign("team_id", Team.table);
636
- this.dropIndex("team_id");
637
- this.dropColumn("team_id");
723
+ {
724
+ down() {
725
+ this.dropForeign("team_id", Team.table);
726
+ this.dropIndex("team_id");
727
+ this.dropColumn("team_id");
728
+ },
638
729
  },
639
- });
730
+ );
640
731
  ```
641
732
 
642
733
  ### Swap unique → plain index
643
734
 
644
735
  ```ts
645
- export default Migration.alter(User, {
646
- dropUnique: [["email"]],
647
- addIndex: [{ columns: "email", name: "users_email_index" }],
648
- }, {
649
- down() {
650
- this.dropIndex("users_email_index");
651
- this.unique("email");
736
+ export default Migration.alter(
737
+ User,
738
+ {
739
+ dropUnique: [["email"]],
740
+ addIndex: [{ columns: "email", name: "users_email_index" }],
652
741
  },
653
- });
742
+ {
743
+ down() {
744
+ this.dropIndex("users_email_index");
745
+ this.unique("email");
746
+ },
747
+ },
748
+ );
654
749
  ```
655
750
 
656
751
  ### Rename + widen a column
657
752
 
658
753
  ```ts
659
- export default Migration.alter(Post, {
660
- rename: { body: "content" },
661
- modify: { content: text().notNullable() },
662
- }, {
663
- down() {
664
- this.renameColumn("content", "body");
754
+ export default Migration.alter(
755
+ Post,
756
+ {
757
+ rename: { body: "content" },
758
+ modify: { content: text().notNullable() },
665
759
  },
666
- });
760
+ {
761
+ down() {
762
+ this.renameColumn("content", "body");
763
+ },
764
+ },
765
+ );
667
766
  ```
668
767
 
669
768
  ### Multiple operations in one migration
@@ -671,22 +770,26 @@ export default Migration.alter(Post, {
671
770
  Combine freely — all operations run in a single `up()` call:
672
771
 
673
772
  ```ts
674
- export default Migration.alter(Product, {
675
- add: { sku: text().unique().notNullable() },
676
- drop: ["old_code"],
677
- rename: { price: "price_cents" },
678
- modify: { price_cents: integer().notNullable() },
679
- addIndex: [{ columns: ["category_id", "status"], name: "idx_cat_status" }],
680
- addCheck: [{ name: "price_non_negative", expression: "price_cents >= 0" }],
681
- }, {
682
- down() {
683
- this.dropCheck("price_non_negative");
684
- this.dropIndex("idx_cat_status");
685
- this.renameColumn("price_cents", "price");
686
- this.text("old_code").nullable();
687
- this.dropColumn("sku");
773
+ export default Migration.alter(
774
+ Product,
775
+ {
776
+ add: { sku: text().unique().notNullable() },
777
+ drop: ["old_code"],
778
+ rename: { price: "price_cents" },
779
+ modify: { price_cents: integer().notNullable() },
780
+ addIndex: [{ columns: ["category_id", "status"], name: "idx_cat_status" }],
781
+ addCheck: [{ name: "price_non_negative", expression: "price_cents >= 0" }],
688
782
  },
689
- });
783
+ {
784
+ down() {
785
+ this.dropCheck("price_non_negative");
786
+ this.dropIndex("idx_cat_status");
787
+ this.renameColumn("price_cents", "price");
788
+ this.text("old_code").nullable();
789
+ this.dropColumn("sku");
790
+ },
791
+ },
792
+ );
690
793
  ```
691
794
 
692
795
  ---
@@ -768,12 +871,12 @@ export class User extends Model<UserSchema> {
768
871
 
769
872
  // Write
770
873
  const user = await User.create({ name: "Ada Lovelace", email: "ada@example.com" });
771
- user.id; // generated ID — direct property
772
- user.get("status"); // "active" — default fired
874
+ user.id; // generated ID — direct property
875
+ user.get("status"); // "active" — default fired
773
876
 
774
877
  // Read
775
878
  const found = await User.find(user.id);
776
- found?.get("email"); // "ada@example.com"
879
+ found?.get("email"); // "ada@example.com"
777
880
 
778
881
  // Filter
779
882
  const active = await User.where("status", "active").get();
@@ -782,22 +885,22 @@ const page = await User.paginate({ page: 1, limit: 20 });
782
885
 
783
886
  ## Pick a skill
784
887
 
785
- | If the task is about… | Load |
786
- | --- | --- |
787
- | Defining a model class — schema, decorators, `Model<TSchema>`, accessors | The `define-model` topic |
788
- | Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering | The `query-data` topic |
789
- | Pagination — `.paginate`, `cursorPaginate`, `chunk` | The `paginate-results` topic |
790
- | Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading | The `define-relations` topic |
791
- | Migrations — `migration` definition, `up`/`down`, CLI | The `write-migration` topic |
792
- | Transactions — `transaction(fn)`, rollback, isolation | The `manage-transactions` topic |
793
- | Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` | The `track-changes` topic |
794
- | Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` | The `subscribe-to-model-events` topic |
795
- | Soft / hard / trash deletes + restore | The `configure-delete-strategy` topic |
796
- | Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` | The `perform-atomic-ops` topic |
797
- | Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` | The `aggregate-data` topic |
798
- | Vector search — `similarTo`, pgvector / Atlas vector index | The `search-by-vector` topic |
799
- | Multiple databases — `connectToDatabase`, per-model `static dataSource` | The `manage-data-sources` topic |
800
- | CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic | The `run-cascade-cli` topic |
888
+ | If the task is about… | Load |
889
+ | ---------------------------------------------------------------------------------------- | ------------------------------------- |
890
+ | Defining a model class — schema, decorators, `Model<TSchema>`, accessors | The `define-model` topic |
891
+ | Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering | The `query-data` topic |
892
+ | Pagination — `.paginate`, `cursorPaginate`, `chunk` | The `paginate-results` topic |
893
+ | Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading | The `define-relations` topic |
894
+ | Migrations — `migration` definition, `up`/`down`, CLI | The `write-migration` topic |
895
+ | Transactions — `transaction(fn)`, rollback, isolation | The `manage-transactions` topic |
896
+ | Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` | The `track-changes` topic |
897
+ | Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` | The `subscribe-to-model-events` topic |
898
+ | Soft / hard / trash deletes + restore | The `configure-delete-strategy` topic |
899
+ | Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` | The `perform-atomic-ops` topic |
900
+ | Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` | The `aggregate-data` topic |
901
+ | Vector search — `similarTo`, pgvector / Atlas vector index | The `search-by-vector` topic |
902
+ | Multiple databases — `connectToDatabase`, per-model `static dataSource` | The `manage-data-sources` topic |
903
+ | CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic | The `run-cascade-cli` topic |
801
904
 
802
905
  ## Things NOT to do
803
906
 
@@ -824,11 +927,11 @@ description: 'Pick the delete behavior — `permanent` (hard delete), `soft` (se
824
927
 
825
928
  `DeleteStrategy` is `"permanent" | "soft" | "trash"`.
826
929
 
827
- | Strategy | Behavior | Reversible? |
828
- | --- | --- | --- |
829
- | `permanent` | DELETE the row / document (the default fallback) | No |
830
- | `soft` | Set the `deletedAt` column; the row stays in the table | Yes — via `Model.restore(id)` |
831
- | `trash` | Move the record to a separate table / collection, then delete the original | Yes — via `Model.restore(id)` |
930
+ | Strategy | Behavior | Reversible? |
931
+ | ----------- | -------------------------------------------------------------------------- | ----------------------------- |
932
+ | `permanent` | DELETE the row / document (the default fallback) | No |
933
+ | `soft` | Set the `deletedAt` column; the row stays in the table | Yes — via `Model.restore(id)` |
934
+ | `trash` | Move the record to a separate table / collection, then delete the original | Yes — via `Model.restore(id)` |
832
935
 
833
936
  ## Set the default per-model
834
937
 
@@ -849,9 +952,9 @@ When the strategy resolves to `"soft"`, `Migration.create(Model, { … })` adds
849
952
  ## Override per-call
850
953
 
851
954
  ```ts
852
- await user.destroy(); // uses the model's strategy
853
- await user.destroy({ strategy: "permanent" }); // GDPR-compliant hard delete
854
- await user.destroy({ strategy: "trash" }); // move to the trash table
955
+ await user.destroy(); // uses the model's strategy
956
+ await user.destroy({ strategy: "permanent" }); // GDPR-compliant hard delete
957
+ await user.destroy({ strategy: "trash" }); // move to the trash table
855
958
  ```
856
959
 
857
960
  Resolution order: `destroy({ strategy })` → `static deleteStrategy` → data source `defaultDeleteStrategy` → `"permanent"`.
@@ -863,10 +966,10 @@ After a `"soft"` destroy the in-memory instance reflects the change — `model.g
863
966
  Restore is a **static** operation keyed by the record's id. It auto-detects whether the record was soft-deleted or trashed:
864
967
 
865
968
  ```ts
866
- const user = await User.restore(123); // clears deletedAt or pulls from trash; fires "restored"
969
+ const user = await User.restore(123); // clears deletedAt or pulls from trash; fires "restored"
867
970
  const user2 = await User.restore(123, { onIdConflict: "fail" });
868
971
 
869
- await User.restoreAll(); // restore every deleted record for this model
972
+ await User.restoreAll(); // restore every deleted record for this model
870
973
  ```
871
974
 
872
975
  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.
@@ -904,7 +1007,7 @@ export class User extends Model<UserSchema> {
904
1007
  With that scope in place:
905
1008
 
906
1009
  ```ts
907
- await User.all(); // active only (scope applied)
1010
+ await User.all(); // active only (scope applied)
908
1011
  await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-deleted
909
1012
  await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
910
1013
  ```
@@ -995,10 +1098,10 @@ export class User extends Model<UserSchema> {
995
1098
  ## Read state — `.id`, `.uuid`, and `.get(field)`
996
1099
 
997
1100
  ```ts
998
- user.id; // direct property — ID is so common cascade exposes it directly
999
- user.uuid; // the same id, narrowed to `string` (see below)
1000
- user.get("status"); // canonical reader for every other column
1001
- user.get<number>("age"); // TypeScript generic for typed reads
1101
+ user.id; // direct property — ID is so common cascade exposes it directly
1102
+ user.uuid; // the same id, narrowed to `string` (see below)
1103
+ user.get("status"); // canonical reader for every other column
1104
+ user.get<number>("age"); // TypeScript generic for typed reads
1002
1105
  ```
1003
1106
 
1004
1107
  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.
@@ -1007,18 +1110,20 @@ Use the direct `.id` property; use `.get("field")` for everything else. Add a ty
1007
1110
 
1008
1111
  Both getters return the model's primary id (`this.get("id")`); the only difference is the **static type**:
1009
1112
 
1010
- | Accessor | Return type | Use when |
1011
- |---|---|---|
1012
- | `model.id` | `string \| number` | You want the real id type — SQL auto-increment is `number`, MongoDB's ObjectId is `string`. |
1013
- | `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. |
1113
+ | Accessor | Return type | Use when |
1114
+ | ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
1115
+ | `model.id` | `string \| number` | You want the real id type — SQL auto-increment is `number`, MongoDB's ObjectId is `string`. |
1116
+ | `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. |
1014
1117
 
1015
1118
  `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:
1016
1119
 
1017
1120
  ```ts
1018
- function shareLink(modelId: string) { /* ... */ }
1121
+ function shareLink(modelId: string) {
1122
+ /* ... */
1123
+ }
1019
1124
 
1020
- shareLink(user.uuid); // ✓ typechecks on both Postgres and MongoDB
1021
- shareLink(user.id); // ✗ TS error on SQL: `number` is not assignable to `string`
1125
+ shareLink(user.uuid); // ✓ typechecks on both Postgres and MongoDB
1126
+ shareLink(user.id); // ✗ TS error on SQL: `number` is not assignable to `string`
1022
1127
  ```
1023
1128
 
1024
1129
  ## Write — three update idioms
@@ -1041,7 +1146,7 @@ await user.merge({ name: "Augusta Ada King", status: "active" }).save();
1041
1146
 
1042
1147
  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.
1043
1148
 
1044
- **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()`.
1149
+ **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()`.
1045
1150
 
1046
1151
  ### `.save()` after manual mutation — when changes are spread
1047
1152
 
@@ -1057,11 +1162,11 @@ await user.save();
1057
1162
 
1058
1163
  Quick reference:
1059
1164
 
1060
- | Situation | Pattern |
1061
- | --- | --- |
1062
- | 1–2 specific fields | `user.set(k, v).save()` |
1063
- | Bulk from an object | `user.merge(data).save()` |
1064
- | After spread mutations | `user.save()` |
1165
+ | Situation | Pattern |
1166
+ | ---------------------- | ------------------------- |
1167
+ | 1–2 specific fields | `user.set(k, v).save()` |
1168
+ | Bulk from an object | `user.merge(data).save()` |
1169
+ | After spread mutations | `user.save()` |
1065
1170
 
1066
1171
  ## Remove a field — `.unset(field)`
1067
1172
 
@@ -1206,7 +1311,7 @@ export class User extends Model<UserSchema> {
1206
1311
 
1207
1312
  @BelongsToMany("Role", {
1208
1313
  pivot: "user_roles",
1209
- localKey: "user_id", // pivot column → this model (User)
1314
+ localKey: "user_id", // pivot column → this model (User)
1210
1315
  foreignKey: "role_id", // pivot column → the related model (Role)
1211
1316
  })
1212
1317
  public roles!: Role[];
@@ -1220,12 +1325,12 @@ export class User extends Model<UserSchema> {
1220
1325
  Manage pivot rows through `model.pivot(relation)`, which returns the relation's `PivotOperations` handle:
1221
1326
 
1222
1327
  ```ts
1223
- await user.pivot("roles").attach([adminId, editorId]); // add (skips existing)
1328
+ await user.pivot("roles").attach([adminId, editorId]); // add (skips existing)
1224
1329
  await user.pivot("roles").attach([adminId], { addedBy: actorId }); // + extra pivot columns
1225
- await user.pivot("roles").detach([editorId]); // remove a subset
1226
- await user.pivot("roles").detach(); // remove all
1227
- await user.pivot("roles").sync([adminId]); // replace the whole set
1228
- await user.pivot("roles").toggle([adminId]); // flip each id
1330
+ await user.pivot("roles").detach([editorId]); // remove a subset
1331
+ await user.pivot("roles").detach(); // remove all
1332
+ await user.pivot("roles").sync([adminId]); // replace the whole set
1333
+ await user.pivot("roles").toggle([adminId]); // flip each id
1229
1334
  ```
1230
1335
 
1231
1336
  `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.
@@ -1238,7 +1343,7 @@ Load related models in the same query, avoiding N+1:
1238
1343
  const posts = await Post.with("author").get();
1239
1344
 
1240
1345
  for (const post of posts) {
1241
- post.getRelation("author"); // already loaded — no second query
1346
+ post.getRelation("author"); // already loaded — no second query
1242
1347
  }
1243
1348
  ```
1244
1349
 
@@ -1260,11 +1365,9 @@ await User.with("posts.comments").get();
1260
1365
  Filter parents based on conditions on children:
1261
1366
 
1262
1367
  ```ts
1263
- const usersWithPublishedPosts = await User
1264
- .whereHas("posts", (query) => {
1265
- query.where("status", "published");
1266
- })
1267
- .get();
1368
+ const usersWithPublishedPosts = await User.whereHas("posts", (query) => {
1369
+ query.where("status", "published");
1370
+ }).get();
1268
1371
  ```
1269
1372
 
1270
1373
  `.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."
@@ -1287,8 +1390,8 @@ Why not `post.set("author", currentUser)`? `set` treats the value as a column wr
1287
1390
  ```ts
1288
1391
  const post = await Post.with("author").first();
1289
1392
 
1290
- post.getRelation("author"); // typed access to the loaded User
1291
- post.author; // direct field — only if you declared it as a typed field
1393
+ post.getRelation("author"); // typed access to the loaded User
1394
+ post.author; // direct field — only if you declared it as a typed field
1292
1395
 
1293
1396
  // Without eager loading — load on demand, then read:
1294
1397
  await post.load("author");
@@ -1302,10 +1405,7 @@ const author = post.getRelation("author");
1302
1405
  `.with()` runs separate queries and hydrates relations. `.joinWith()` joins at the SQL/aggregation level:
1303
1406
 
1304
1407
  ```ts
1305
- const result = await User
1306
- .joinWith("posts")
1307
- .where("posts.status", "published")
1308
- .get();
1408
+ const result = await User.joinWith("posts").where("posts.status", "published").get();
1309
1409
  ```
1310
1410
 
1311
1411
  Use `.joinWith` when you need conditions across both tables in a single query. Use `.with` when you want hydrated relation models without joining.
@@ -1604,9 +1704,12 @@ Throwing works too (and re-throws to the caller); `ctx.rollback()` is the non-th
1604
1704
  Default is the driver's default (typically `READ COMMITTED`). Request a different level via `isolationLevel`:
1605
1705
 
1606
1706
  ```ts
1607
- await transaction(async () => {
1608
- /* ... */
1609
- }, { isolationLevel: "SERIALIZABLE" });
1707
+ await transaction(
1708
+ async () => {
1709
+ /* ... */
1710
+ },
1711
+ { isolationLevel: "SERIALIZABLE" },
1712
+ );
1610
1713
  ```
1611
1714
 
1612
1715
  On `SERIALIZABLE`, Postgres may abort with a serialization failure when concurrent transactions conflict — wrap in retry logic at the caller.
@@ -1616,21 +1719,24 @@ On `SERIALIZABLE`, Postgres may abort with a serialization failure when concurre
1616
1719
  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:
1617
1720
 
1618
1721
  ```ts
1619
- await transaction(async () => {
1620
- await Invoice.query().get();
1621
- }, {
1622
- settings: {
1623
- "app.tenant_id": tenantId,
1624
- statement_timeout: 5_000,
1722
+ await transaction(
1723
+ async () => {
1724
+ await Invoice.query().get();
1625
1725
  },
1626
- });
1726
+ {
1727
+ settings: {
1728
+ "app.tenant_id": tenantId,
1729
+ statement_timeout: 5_000,
1730
+ },
1731
+ },
1732
+ );
1627
1733
  ```
1628
1734
 
1629
1735
  ## Row locking — `lockForUpdate({ skipLocked })` (Postgres)
1630
1736
 
1631
1737
  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.
1632
1738
 
1633
- `{ 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.
1739
+ `{ 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.
1634
1740
 
1635
1741
  ```ts
1636
1742
  import { transaction } from "@warlock.js/cascade";
@@ -1696,7 +1802,7 @@ const { rows: counts } = await dataSource.raw<{ count: number }>(
1696
1802
 
1697
1803
  ### Native-array columns (`JSONB[]` / `TEXT[]` / `INTEGER[]`)
1698
1804
 
1699
- 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"*.
1805
+ 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"_.
1700
1806
 
1701
1807
  **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.
1702
1808
 
@@ -1763,8 +1869,8 @@ The everyday case for listings with page numbers:
1763
1869
  ```ts
1764
1870
  const page = await User.paginate({ page: 1, limit: 20 });
1765
1871
 
1766
- page.data; // User[]
1767
- page.pagination; // { total, page, limit, pages }
1872
+ page.data; // User[]
1873
+ page.pagination; // { total, page, limit, pages }
1768
1874
  ```
1769
1875
 
1770
1876
  `PaginationOptions` is `{ page?, limit? }` — there is no `filter` field; filter by chaining `.where()` before `.paginate()` (below).
@@ -1772,9 +1878,7 @@ page.pagination; // { total, page, limit, pages }
1772
1878
  Chain off `.where()` for filtered pagination:
1773
1879
 
1774
1880
  ```ts
1775
- const activePage = await User
1776
- .where("status", "active")
1777
- .paginate({ page: 2, limit: 20 });
1881
+ const activePage = await User.where("status", "active").paginate({ page: 2, limit: 20 });
1778
1882
  ```
1779
1883
 
1780
1884
  **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.
@@ -1786,9 +1890,9 @@ For very large datasets where deep pagination matters:
1786
1890
  ```ts
1787
1891
  const first = await User.query().orderBy("created_at", "desc").cursorPaginate({ limit: 20 });
1788
1892
 
1789
- first.data; // User[]
1790
- first.pagination.nextCursor; // opaque value — pass to the next call
1791
- first.pagination.hasMore; // boolean
1893
+ first.data; // User[]
1894
+ first.pagination.nextCursor; // opaque value — pass to the next call
1895
+ first.pagination.hasMore; // boolean
1792
1896
 
1793
1897
  const next = await User.query()
1794
1898
  .orderBy("created_at", "desc")
@@ -1894,10 +1998,13 @@ Signature: `Model.increase(filter, field, amount)` / `Model.decrease(filter, fie
1894
1998
  ## Arbitrary atomic mutations — `Model.atomic`
1895
1999
 
1896
2000
  ```ts
1897
- await User.atomic({ id: userId }, {
1898
- $set: { last_seen: new Date() },
1899
- $inc: { login_count: 1 },
1900
- });
2001
+ await User.atomic(
2002
+ { id: userId },
2003
+ {
2004
+ $set: { last_seen: new Date() },
2005
+ $inc: { login_count: 1 },
2006
+ },
2007
+ );
1901
2008
  ```
1902
2009
 
1903
2010
  `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.
@@ -1923,7 +2030,11 @@ const granted = await Quota.atomic(
1923
2030
 
1924
2031
  // MongoDB only: pipeline update, $addToSet, arrayFilters
1925
2032
  await Post.findOneAndUpdate({ slug }, [{ $set: { score: { $add: ["$likes", "$shares"] } } }]);
1926
- await Post.atomic({ slug }, { $set: { "grades.$[low].score": 50 } }, { arrayFilters: [{ "low.score": { $lt: 50 } }] });
2033
+ await Post.atomic(
2034
+ { slug },
2035
+ { $set: { "grades.$[low].score": 50 } },
2036
+ { arrayFilters: [{ "low.score": { $lt: 50 } }] },
2037
+ );
1927
2038
  ```
1928
2039
 
1929
2040
  - `atomic(filter, update, { upsert?, arrayFilters?, trustedFilter? })` → modified + upserted count.
@@ -1931,6 +2042,7 @@ await Post.atomic({ slug }, { $set: { "grades.$[low].score": 50 } }, { arrayFilt
1931
2042
  - `findAndUpdate(filter, update, { upsert?, arrayFilters? })`. No `trustedFilter`, because it reads the rows back with `where(filter)`.
1932
2043
 
1933
2044
  **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:
2045
+
1934
2046
  - pipeline updates
1935
2047
  - `arrayFilters`
1936
2048
  - `$push`, `$pull`, `$addToSet` and unknown operators
@@ -1973,10 +2085,7 @@ const rows = await OrderItem.createMany(millionRows, { bulk: true, batchSize: 10
1973
2085
  ## Bulk update — `Model.findAndUpdate(filter, operations)`
1974
2086
 
1975
2087
  ```ts
1976
- const updated = await User.findAndUpdate(
1977
- { status: "pending" },
1978
- { $set: { status: "active" } },
1979
- );
2088
+ const updated = await User.findAndUpdate({ status: "pending" }, { $set: { status: "active" } });
1980
2089
  // updated: User[] — the matched-and-updated models
1981
2090
  ```
1982
2091
 
@@ -1987,8 +2096,8 @@ const updated = await User.findAndUpdate(
1987
2096
  ## Bulk delete — `Model.delete(filter)`
1988
2097
 
1989
2098
  ```ts
1990
- await User.delete({ status: "spam" }); // delete all matching → count
1991
- await User.deleteOne({ status: "spam" }); // delete the first match → count
2099
+ await User.delete({ status: "spam" }); // delete all matching → count
2100
+ await User.deleteOne({ status: "spam" }); // delete the first match → count
1992
2101
  ```
1993
2102
 
1994
2103
  `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.
@@ -2004,16 +2113,16 @@ for (const user of targets) {
2004
2113
 
2005
2114
  ## When to reach for what
2006
2115
 
2007
- | Task | Reach for |
2008
- | --- | --- |
2009
- | Increment a counter | `Model.increase(filter, field, n)` |
2010
- | Atomically change multiple fields on one record | `Model.atomic(filter, ops)` |
2011
- | Insert N records | `Model.createMany(rows)` |
2012
- | Insert a large batch fast (no per-row events) | `Model.createMany(rows, { bulk: true, batchSize })` |
2013
- | Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
2014
- | Update one record by id | `Model.update(id, data)` |
2015
- | Delete many rows (raw) | `Model.delete(filter)` |
2016
- | Multi-row read-modify-write | Wrap in a transaction (see the `manage-transactions` topic) |
2116
+ | Task | Reach for |
2117
+ | ----------------------------------------------- | ------------------------------------------------------------- |
2118
+ | Increment a counter | `Model.increase(filter, field, n)` |
2119
+ | Atomically change multiple fields on one record | `Model.atomic(filter, ops)` |
2120
+ | Insert N records | `Model.createMany(rows)` |
2121
+ | Insert a large batch fast (no per-row events) | `Model.createMany(rows, { bulk: true, batchSize })` |
2122
+ | Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
2123
+ | Update one record by id | `Model.update(id, data)` |
2124
+ | Delete many rows (raw) | `Model.delete(filter)` |
2125
+ | Multi-row read-modify-write | Wrap in a transaction (see the `manage-transactions` topic) |
2017
2126
  | Need lifecycle events / delete strategy per row | `Model.where(...).get()` + iterate + `.save()` / `.destroy()` |
2018
2127
 
2019
2128
  ## Things NOT to do
@@ -2055,9 +2164,9 @@ const activeUsers = await User.where("status", "active").get();
2055
2164
  ### Operators
2056
2165
 
2057
2166
  ```ts
2058
- const adults = await User.where("age", ">", 18).get();
2059
- const recent = await User.where("created_at", ">=", lastWeek).get();
2060
- const nonAdmins = await User.where("role", "!=", "admin").get();
2167
+ const adults = await User.where("age", ">", 18).get();
2168
+ const recent = await User.where("created_at", ">=", lastWeek).get();
2169
+ const nonAdmins = await User.where("role", "!=", "admin").get();
2061
2170
  ```
2062
2171
 
2063
2172
  3-argument form. Common operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `notIn`, `like`, `between`. Same syntax across MongoDB and Postgres.
@@ -2065,10 +2174,7 @@ const nonAdmins = await User.where("role", "!=", "admin").get();
2065
2174
  ### Compound conditions
2066
2175
 
2067
2176
  ```ts
2068
- const activeAdmins = await User
2069
- .where("status", "active")
2070
- .where("role", "admin")
2071
- .get();
2177
+ const activeAdmins = await User.where("status", "active").where("role", "admin").get();
2072
2178
  ```
2073
2179
 
2074
2180
  Chained `.where()` calls combine with `AND`.
@@ -2093,7 +2199,7 @@ await User.first({ email, password }); // throws UnsafeFilterError if password i
2093
2199
 
2094
2200
  ```ts
2095
2201
  query.whereRaw({ $expr: { $gt: ["$stock", "$reserved"] } }); // object form — MongoDB
2096
- query.whereRaw("age > ?", [30]); // string form — SQL, real bindings
2202
+ query.whereRaw("age > ?", [30]); // string form — SQL, real bindings
2097
2203
  ```
2098
2204
 
2099
2205
  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).
@@ -2103,15 +2209,15 @@ The **object form** is checked like any other filter value, plus it rejects the
2103
2209
  ### By ID
2104
2210
 
2105
2211
  ```ts
2106
- const user = await User.find(id); // User | null
2212
+ const user = await User.find(id); // User | null
2107
2213
  ```
2108
2214
 
2109
2215
  ### First match
2110
2216
 
2111
2217
  ```ts
2112
- const anyUser = await User.first(); // first user, any
2113
- const firstAdmin = await User.first({ role: "admin" }); // first admin
2114
- const filtered = await User.where("status", "active").first(); // chain into .first()
2218
+ const anyUser = await User.first(); // first user, any
2219
+ const firstAdmin = await User.first({ role: "admin" }); // first admin
2220
+ const filtered = await User.where("status", "active").first(); // chain into .first()
2115
2221
  ```
2116
2222
 
2117
2223
  `.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.
@@ -2129,10 +2235,7 @@ Throws when nothing matches — useful when you KNOW it should exist and want th
2129
2235
  ## Order and paginate
2130
2236
 
2131
2237
  ```ts
2132
- const newest = await User
2133
- .where("status", "active")
2134
- .orderBy("created_at", "desc")
2135
- .get();
2238
+ const newest = await User.where("status", "active").orderBy("created_at", "desc").get();
2136
2239
  ```
2137
2240
 
2138
2241
  `.orderBy(field, "asc" | "desc")` sorts. Default direction is `"asc"`. Chain multiple `.orderBy()` for tiebreakers.
@@ -2151,11 +2254,11 @@ rows[0].name; // typed as UserSchema, not a User instance
2151
2254
  ## Count and existence
2152
2255
 
2153
2256
  ```ts
2154
- const total = await User.count();
2257
+ const total = await User.count();
2155
2258
  const activeCount = await User.count({ status: "active" });
2156
- const adminCount = await User.where("role", "admin").count();
2259
+ const adminCount = await User.where("role", "admin").count();
2157
2260
 
2158
- const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
2261
+ const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
2159
2262
  const noneBlocked = await User.where("status", "blocked").notExists();
2160
2263
  ```
2161
2264
 
@@ -2164,7 +2267,7 @@ const noneBlocked = await User.where("status", "blocked").notExists();
2164
2267
  ## Get many — `.all(filter?)`
2165
2268
 
2166
2269
  ```ts
2167
- const allUsers = await User.all();
2270
+ const allUsers = await User.all();
2168
2271
  const activeUsers = await User.all({ status: "active" });
2169
2272
  ```
2170
2273
 
@@ -2176,19 +2279,19 @@ const activeUsers = await User.all({ status: "active" });
2176
2279
 
2177
2280
  Cascade's query builder has around 60 methods. Reach for these as the need arises:
2178
2281
 
2179
- | Reach for | When |
2180
- | --- | --- |
2181
- | `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
2182
- | `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
2183
- | `.whereBetween(field, [a, b])` | Inclusive range |
2184
- | `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
2185
- | `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
2186
- | `.whereHas(relation, callback)` | Filter by conditions on a related model |
2187
- | `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — the `aggregate-data` topic |
2188
- | `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
2189
- | `.chunk(size, callback)` | Stream a large table in batches |
2190
- | `.cursorPaginate({ limit, cursor })` | Cursor pagination — the `paginate-results` topic |
2191
- | `.similarTo(column, embedding)` | Vector similarity — the `search-by-vector` topic |
2282
+ | Reach for | When |
2283
+ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
2284
+ | `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
2285
+ | `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
2286
+ | `.whereBetween(field, [a, b])` | Inclusive range |
2287
+ | `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
2288
+ | `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
2289
+ | `.whereHas(relation, callback)` | Filter by conditions on a related model |
2290
+ | `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — the `aggregate-data` topic |
2291
+ | `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
2292
+ | `.chunk(size, callback)` | Stream a large table in batches |
2293
+ | `.cursorPaginate({ limit, cursor })` | Cursor pagination — the `paginate-results` topic |
2294
+ | `.similarTo(column, embedding)` | Vector similarity — the `search-by-vector` topic |
2192
2295
 
2193
2296
  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`.)
2194
2297
 
@@ -2271,6 +2374,7 @@ cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
2271
2374
  ```
2272
2375
 
2273
2376
  Flags:
2377
+
2274
2378
  - `migrate` — `-f/--fresh` (drop everything and re-run), `-s/--sql` (export SQL instead of executing), `--pending-only`, `-c/--compact`, `-p/--path <glob>`
2275
2379
  - `migrate:rollback` — `-a/--all` (roll back everything), `--batches N`, `-p/--path <glob>`
2276
2380
  - `migrate:export-sql` — `--pending-only`, `-c/--compact`, `-p/--path <glob>`
@@ -2354,16 +2458,16 @@ Reach for these when you need migrations inside test setup (`beforeAll(async ()
2354
2458
 
2355
2459
  ## Common task → command
2356
2460
 
2357
- | You want to… | Command |
2358
- |---|---|
2359
- | Run all pending migrations | `cascade migrate` |
2360
- | Drop everything and re-run | `cascade migrate -f` |
2361
- | Roll back the last batch | `cascade migrate:rollback` |
2362
- | Roll back everything | `cascade migrate:rollback --all` |
2363
- | Generate SQL without executing | `cascade migrate --sql` |
2364
- | Generate SQL for pending only | `cascade migrate --sql --pending-only` |
2365
- | See what's been executed | `cascade migrate:list` |
2366
- | Run from a non-default folder | `cascade migrate -p "<glob>"` |
2461
+ | You want to… | Command |
2462
+ | ------------------------------ | -------------------------------------- |
2463
+ | Run all pending migrations | `cascade migrate` |
2464
+ | Drop everything and re-run | `cascade migrate -f` |
2465
+ | Roll back the last batch | `cascade migrate:rollback` |
2466
+ | Roll back everything | `cascade migrate:rollback --all` |
2467
+ | Generate SQL without executing | `cascade migrate --sql` |
2468
+ | Generate SQL for pending only | `cascade migrate --sql --pending-only` |
2469
+ | See what's been executed | `cascade migrate:list` |
2470
+ | Run from a non-default folder | `cascade migrate -p "<glob>"` |
2367
2471
 
2368
2472
  ## Things NOT to do
2369
2473
 
@@ -2477,7 +2581,10 @@ async function answer(question: string) {
2477
2581
  // optional: drop low-similarity hits in code
2478
2582
  const relevant = context.filter((document) => document.score >= 0.75);
2479
2583
 
2480
- const prompt = buildPrompt(question, relevant.map((document) => document.body));
2584
+ const prompt = buildPrompt(
2585
+ question,
2586
+ relevant.map((document) => document.body),
2587
+ );
2481
2588
  return ai.complete(prompt);
2482
2589
  }
2483
2590
  ```
@@ -2486,11 +2593,11 @@ A score threshold isn't a query option — filter on the `score` column in code
2486
2593
 
2487
2594
  ## Driver support
2488
2595
 
2489
- | Driver | Vector |
2490
- | --- | --- |
2491
- | Postgres (with `CREATE EXTENSION vector`) | ✅ pgvector + IVFFlat index |
2596
+ | Driver | Vector |
2597
+ | ---------------------------------------------- | ------------------------------------ |
2598
+ | Postgres (with `CREATE EXTENSION vector`) | ✅ pgvector + IVFFlat index |
2492
2599
  | MongoDB Atlas (paid tier + Atlas Search index) | ✅ `$vectorSearch` aggregation stage |
2493
- | MongoDB community / self-hosted / local | ❌ Atlas-only |
2600
+ | MongoDB community / self-hosted / local | ❌ Atlas-only |
2494
2601
 
2495
2602
  For local dev with MongoDB, develop the vector path against Postgres + pgvector.
2496
2603
 
@@ -2537,18 +2644,18 @@ Listeners are async-aware — the model's persistence awaits the listener before
2537
2644
 
2538
2645
  The events fire in this order. The **gerund** form (`saving`, `creating`) is the "before" hook — there is no `beforeSave` alias.
2539
2646
 
2540
- | Event | Fires |
2541
- | --- | --- |
2542
- | `saving` | Before validation, on every `save()` (both insert and update paths) |
2543
- | `validating` / `validated` | Around schema validation |
2544
- | `creating` | Before a new record is inserted (first save) |
2545
- | `updating` | Before an existing record's update is written |
2546
- | `saved` | After a successful write (any branch — insert or update) |
2547
- | `created` | After a new record is inserted |
2548
- | `updated` | After an existing record's update is written |
2549
- | `deleting` / `deleted` | Before / after the delete strategy runs |
2550
- | `restoring` / `restored` | Before / after a soft-deleted or trashed record is restored |
2551
- | `fetching` / `fetched` / `hydrating` | Around reads / instance hydration |
2647
+ | Event | Fires |
2648
+ | ------------------------------------ | ------------------------------------------------------------------- |
2649
+ | `saving` | Before validation, on every `save()` (both insert and update paths) |
2650
+ | `validating` / `validated` | Around schema validation |
2651
+ | `creating` | Before a new record is inserted (first save) |
2652
+ | `updating` | Before an existing record's update is written |
2653
+ | `saved` | After a successful write (any branch — insert or update) |
2654
+ | `created` | After a new record is inserted |
2655
+ | `updated` | After an existing record's update is written |
2656
+ | `deleting` / `deleted` | Before / after the delete strategy runs |
2657
+ | `restoring` / `restored` | Before / after a soft-deleted or trashed record is restored |
2658
+ | `fetching` / `fetched` / `hydrating` | Around reads / instance hydration |
2552
2659
 
2553
2660
  `saving` fires for both inserts and updates; `creating` / `updating` narrow to the branch. So `saved` always fires; exactly one of `created` / `updated` follows.
2554
2661
 
@@ -2579,17 +2686,17 @@ Pair with `isDirty()` from the `track-changes` topic so the listener only runs i
2579
2686
 
2580
2687
  ## Global listeners — across all models
2581
2688
 
2582
- For framework-level concerns (audit log, observability, cache invalidation), subscribe on the base `Model` — the listener runs for every model:
2689
+ For framework-level concerns (audit log, observability, cache invalidation), subscribe through `Model.globalEvents()` — the listener runs for every model:
2583
2690
 
2584
2691
  ```ts
2585
2692
  import { Model } from "@warlock.js/cascade";
2586
2693
 
2587
- Model.on("saved", async (instance) => {
2588
- await audit.log("save", instance.constructor.name, instance.id, instance.getDirtyColumns());
2694
+ Model.globalEvents().on("saved", (instance) => {
2695
+ console.info("save", instance.constructor.name, instance.id, instance.getDirtyColumns());
2589
2696
  });
2590
2697
  ```
2591
2698
 
2592
- Filter by `instance.constructor.name === "User"` when you only care about specific models, or register per-model handlers when the scope is narrow. (`Model.globalEvents()` returns the underlying global emitter if you need direct access.)
2699
+ Filter by `instance.constructor.name === "User"` when you only care about specific models, or register per-model handlers when the scope is narrow. The base `Model` is abstract; its per-model `on()` shortcut expects a concrete subclass.
2593
2700
 
2594
2701
  ## `off()` to unsubscribe
2595
2702
 
@@ -2627,7 +2734,7 @@ Model.on("updated", async (instance) => {
2627
2734
  });
2628
2735
  ```
2629
2736
 
2630
- Use `updated` (not `saved`) when you only want change diffs — `getDirtyColumnsWithValues()` is still populated in the post-write events because the tracker resets *after* they fire.
2737
+ Use `updated` (not `saved`) when you only want change diffs — `getDirtyColumnsWithValues()` is still populated in the post-write events because the tracker resets _after_ they fire.
2631
2738
 
2632
2739
  ### Cache invalidation
2633
2740
 
@@ -2674,11 +2781,11 @@ Every Cascade model carries a dirty tracker that records which fields you've cha
2674
2781
  ## The main reads
2675
2782
 
2676
2783
  ```ts
2677
- user.hasChanges(); // boolean — any field changed (or removed)?
2678
- user.isDirty("email"); // boolean — specifically this column?
2784
+ user.hasChanges(); // boolean — any field changed (or removed)?
2785
+ user.isDirty("email"); // boolean — specifically this column?
2679
2786
 
2680
- user.getDirtyColumns(); // string[] — names of changed columns
2681
- user.getRemovedColumns(); // string[] — columns explicitly unset since load
2787
+ user.getDirtyColumns(); // string[] — names of changed columns
2788
+ user.getRemovedColumns(); // string[] — columns explicitly unset since load
2682
2789
 
2683
2790
  user.getDirtyColumnsWithValues();
2684
2791
  // Record<string, { oldValue, newValue }> — the full diff, old + new per column
@@ -2723,9 +2830,9 @@ await AuditLog.create({
2723
2830
 
2724
2831
  ```ts
2725
2832
  user.set("name", "Augusta");
2726
- user.isDirty("name"); // true
2833
+ user.isDirty("name"); // true
2727
2834
  await user.save();
2728
- user.isDirty("name"); // false — tracker reset to the just-saved state
2835
+ user.isDirty("name"); // false — tracker reset to the just-saved state
2729
2836
  ```
2730
2837
 
2731
2838
  `save()` resets the tracker's baseline to the persisted state. Subsequent `isDirty()` checks measure changes since the last `save()`, not since the original load.
@@ -2767,12 +2874,12 @@ for (const [field, { oldValue, newValue }] of Object.entries(dirty)) {
2767
2874
 
2768
2875
  ```ts
2769
2876
  // plain object → merged key-by-key, siblings survive
2770
- user.merge({ profile: { age: 26 } }); // profile.city untouched
2771
- user.getDirtyColumns(); // ["profile.age"]
2877
+ user.merge({ profile: { age: 26 } }); // profile.city untouched
2878
+ user.getDirtyColumns(); // ["profile.age"]
2772
2879
 
2773
2880
  // Date → replaced, not merged
2774
2881
  conversation.merge({ lastOutboundAt: new Date() });
2775
- conversation.getDirtyColumns(); // ["lastOutboundAt"]
2882
+ conversation.getDirtyColumns(); // ["lastOutboundAt"]
2776
2883
  ```
2777
2884
 
2778
2885
  :::note[Fixed in 4.9.1]
@@ -2924,10 +3031,10 @@ Class-form builders include `createTable()` / `createTableIfNotExists()`, `dropT
2924
3031
  `this.foreignId(name)` (5.8+) derives the column's type from `migrationDefaults.primaryKey` so a foreign-key column always matches the type of the `id` it points at, instead of hardcoding one:
2925
3032
 
2926
3033
  | `migrationDefaults.primaryKey` | `foreignId` produces |
2927
- | --- | --- |
2928
- | `uuid` | `uuid` column |
2929
- | `bigInt` | `bigInteger` column |
2930
- | anything else (default) | `integer` column |
3034
+ | ------------------------------ | -------------------- |
3035
+ | `uuid` | `uuid` column |
3036
+ | `bigInt` | `bigInteger` column |
3037
+ | anything else (default) | `integer` column |
2931
3038
 
2932
3039
  ```ts
2933
3040
  import { Migration } from "@warlock.js/cascade";