@warlock.js/cascade 5.20.0 → 5.22.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 (172) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/cjs/index.cjs +2460 -1049
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/query-builder.contract.d.mts +30 -3
  5. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  6. package/esm/data-source/data-source-registry.d.mts +33 -1
  7. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  8. package/esm/data-source/data-source-registry.mjs +75 -5
  9. package/esm/data-source/data-source-registry.mjs.map +1 -1
  10. package/esm/database-dirty-tracker.d.mts +5 -0
  11. package/esm/database-dirty-tracker.d.mts.map +1 -1
  12. package/esm/database-dirty-tracker.mjs +16 -0
  13. package/esm/database-dirty-tracker.mjs.map +1 -1
  14. package/esm/drivers/mongodb/mongodb-driver.d.mts +12 -0
  15. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  16. package/esm/drivers/mongodb/mongodb-driver.mjs +47 -16
  17. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -1
  19. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  20. package/esm/drivers/mongodb/mongodb-migration-driver.mjs +15 -18
  21. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  22. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +13 -3
  23. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  24. package/esm/drivers/mongodb/mongodb-query-builder.mjs +72 -19
  25. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  26. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +6 -0
  27. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  28. package/esm/drivers/mongodb/mongodb-query-parser.mjs +128 -30
  29. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  30. package/esm/drivers/postgres/index.d.mts +2 -3
  31. package/esm/drivers/postgres/index.mjs +1 -2
  32. package/esm/drivers/postgres/postgres-blueprint.mjs +4 -4
  33. package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
  34. package/esm/drivers/postgres/postgres-dialect.d.mts +5 -1
  35. package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
  36. package/esm/drivers/postgres/postgres-dialect.mjs +38 -8
  37. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  38. package/esm/drivers/postgres/postgres-driver.d.mts +52 -29
  39. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  40. package/esm/drivers/postgres/postgres-driver.mjs +193 -57
  41. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  42. package/esm/drivers/postgres/postgres-migration-driver.d.mts +2 -1
  43. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  44. package/esm/drivers/postgres/postgres-migration-driver.mjs +13 -6
  45. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  46. package/esm/drivers/postgres/postgres-query-builder.d.mts +18 -7
  47. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  48. package/esm/drivers/postgres/postgres-query-builder.mjs +146 -31
  49. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  50. package/esm/drivers/postgres/postgres-query-parser.d.mts +23 -1
  51. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  52. package/esm/drivers/postgres/postgres-query-parser.mjs +160 -43
  53. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  54. package/esm/drivers/postgres/postgres-sql-serializer.mjs +3 -2
  55. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  56. package/esm/drivers/postgres/postgres-ttl-purge.mjs +98 -0
  57. package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -0
  58. package/esm/drivers/postgres/postgres-type-converters.mjs +57 -0
  59. package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -0
  60. package/esm/events/model-events.d.mts +1 -1
  61. package/esm/events/model-events.d.mts.map +1 -1
  62. package/esm/events/model-events.mjs.map +1 -1
  63. package/esm/index.d.mts +5 -6
  64. package/esm/index.mjs +4 -5
  65. package/esm/migration/column-builder.d.mts.map +1 -1
  66. package/esm/migration/column-builder.mjs +2 -2
  67. package/esm/migration/column-builder.mjs.map +1 -1
  68. package/esm/migration/index.d.mts +2 -2
  69. package/esm/migration/index.mjs +1 -1
  70. package/esm/migration/migration-order.mjs +5 -2
  71. package/esm/migration/migration-order.mjs.map +1 -1
  72. package/esm/migration/migration-runner.d.mts +50 -5
  73. package/esm/migration/migration-runner.d.mts.map +1 -1
  74. package/esm/migration/migration-runner.mjs +369 -146
  75. package/esm/migration/migration-runner.mjs.map +1 -1
  76. package/esm/migration/migration.d.mts +36 -3
  77. package/esm/migration/migration.d.mts.map +1 -1
  78. package/esm/migration/migration.mjs +62 -5
  79. package/esm/migration/migration.mjs.map +1 -1
  80. package/esm/model/methods/accessor-methods.mjs +1 -3
  81. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  82. package/esm/model/methods/delete-methods.mjs +6 -2
  83. package/esm/model/methods/delete-methods.mjs.map +1 -1
  84. package/esm/model/methods/hydration-methods.mjs +16 -2
  85. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  86. package/esm/model/methods/instance-event-methods.mjs +2 -1
  87. package/esm/model/methods/instance-event-methods.mjs.map +1 -1
  88. package/esm/model/methods/meta-methods.mjs +53 -16
  89. package/esm/model/methods/meta-methods.mjs.map +1 -1
  90. package/esm/model/methods/query-methods.mjs +86 -15
  91. package/esm/model/methods/query-methods.mjs.map +1 -1
  92. package/esm/model/methods/static-event-methods.mjs +16 -1
  93. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  94. package/esm/model/methods/write-methods.d.mts.map +1 -1
  95. package/esm/model/methods/write-methods.mjs +50 -50
  96. package/esm/model/methods/write-methods.mjs.map +1 -1
  97. package/esm/model/model.d.mts +16 -1
  98. package/esm/model/model.d.mts.map +1 -1
  99. package/esm/model/model.mjs +39 -5
  100. package/esm/model/model.mjs.map +1 -1
  101. package/esm/model/register-model.d.mts +6 -1
  102. package/esm/model/register-model.d.mts.map +1 -1
  103. package/esm/model/register-model.mjs +16 -1
  104. package/esm/model/register-model.mjs.map +1 -1
  105. package/esm/query-builder/query-builder.d.mts +21 -2
  106. package/esm/query-builder/query-builder.d.mts.map +1 -1
  107. package/esm/query-builder/query-builder.mjs +97 -20
  108. package/esm/query-builder/query-builder.mjs.map +1 -1
  109. package/esm/relations/key-conventions.mjs +13 -3
  110. package/esm/relations/key-conventions.mjs.map +1 -1
  111. package/esm/relations/pivot-operations.mjs +1 -1
  112. package/esm/relations/relation-loader.d.mts +15 -5
  113. package/esm/relations/relation-loader.d.mts.map +1 -1
  114. package/esm/relations/relation-loader.mjs +51 -16
  115. package/esm/relations/relation-loader.mjs.map +1 -1
  116. package/esm/remover/database-remover.d.mts.map +1 -1
  117. package/esm/remover/database-remover.mjs +22 -8
  118. package/esm/remover/database-remover.mjs.map +1 -1
  119. package/esm/restorer/database-restorer.d.mts +11 -2
  120. package/esm/restorer/database-restorer.d.mts.map +1 -1
  121. package/esm/restorer/database-restorer.mjs +45 -13
  122. package/esm/restorer/database-restorer.mjs.map +1 -1
  123. package/esm/sync/model-events.mjs +63 -3
  124. package/esm/sync/model-events.mjs.map +1 -1
  125. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  126. package/esm/sync/model-sync-operation.mjs +3 -2
  127. package/esm/sync/model-sync-operation.mjs.map +1 -1
  128. package/esm/sync/sync-fanout.mjs +67 -0
  129. package/esm/sync/sync-fanout.mjs.map +1 -0
  130. package/esm/sync/sync-manager.d.mts +3 -42
  131. package/esm/sync/sync-manager.d.mts.map +1 -1
  132. package/esm/sync/sync-manager.mjs +37 -126
  133. package/esm/sync/sync-manager.mjs.map +1 -1
  134. package/esm/utils/connect-to-database.d.mts.map +1 -1
  135. package/esm/utils/connect-to-database.mjs +4 -4
  136. package/esm/utils/connect-to-database.mjs.map +1 -1
  137. package/esm/utils/database-writer.utils.d.mts +1 -1
  138. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  139. package/esm/utils/database-writer.utils.mjs +1 -0
  140. package/esm/utils/database-writer.utils.mjs.map +1 -1
  141. package/esm/utils/define-model.d.mts.map +1 -1
  142. package/esm/utils/define-model.mjs +2 -10
  143. package/esm/utils/define-model.mjs.map +1 -1
  144. package/esm/utils/is-valid-date-value.mjs +3 -2
  145. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  146. package/esm/utils/once-connected.d.mts.map +1 -1
  147. package/esm/utils/once-connected.mjs +12 -2
  148. package/esm/utils/once-connected.mjs.map +1 -1
  149. package/esm/validation/mutators/embed-mutator.mjs +19 -11
  150. package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
  151. package/esm/validation/plugins/embed-validator-plugin.d.mts +2 -2
  152. package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
  153. package/esm/validation/plugins/embed-validator-plugin.mjs +8 -2
  154. package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
  155. package/esm/validation/rules/database-model-rule.mjs +6 -5
  156. package/esm/validation/rules/database-model-rule.mjs.map +1 -1
  157. package/esm/validation/rules/exists-rule.mjs +6 -4
  158. package/esm/validation/rules/exists-rule.mjs.map +1 -1
  159. package/esm/validation/rules/unique-rule.mjs +5 -2
  160. package/esm/validation/rules/unique-rule.mjs.map +1 -1
  161. package/esm/validation/validators/embed-validator.d.mts.map +1 -1
  162. package/esm/validation/validators/embed-validator.mjs +1 -1
  163. package/esm/validation/validators/embed-validator.mjs.map +1 -1
  164. package/esm/writer/database-writer.d.mts +27 -0
  165. package/esm/writer/database-writer.d.mts.map +1 -1
  166. package/esm/writer/database-writer.mjs +85 -18
  167. package/esm/writer/database-writer.mjs.map +1 -1
  168. package/package.json +8 -7
  169. package/esm/drivers/postgres/postgres-sync-adapter.d.mts +0 -79
  170. package/esm/drivers/postgres/postgres-sync-adapter.d.mts.map +0 -1
  171. package/esm/drivers/postgres/postgres-sync-adapter.mjs +0 -162
  172. package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +0 -1
@@ -12,9 +12,7 @@ function getFieldValue(model, field, defaultValue) {
12
12
  function setFieldValue(model, field, value) {
13
13
  const path = String(field);
14
14
  set(model.data, path, value);
15
- const partial = {};
16
- set(partial, path, value);
17
- model.dirtyTracker.mergeChanges(partial);
15
+ model.dirtyTracker.setAtPath(path, value);
18
16
  return model;
19
17
  }
20
18
  function hasField(model, field) {
@@ -1 +1 @@
1
- {"version":3,"file":"accessor-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/accessor-methods.ts"],"sourcesContent":["import { get, merge, only, set, unset } from \"@mongez/reinforcements\";\nimport type { Model } from \"../model\";\n\n/**\n * Sentinel symbol to distinguish a genuinely missing field from a field\n * whose value is `undefined`. Encapsulated here — callers use `hasField()`.\n */\nconst MISSING_VALUE = Symbol(\"missing\");\n\nexport function getFieldValue(model: Model, field: string, defaultValue?: unknown): any {\n return get(model.data, field, defaultValue);\n}\n\nexport function setFieldValue(model: Model, field: string, value: unknown): Model {\n const path = String(field);\n set(model.data, path, value);\n\n const partial: Record<string, unknown> = {};\n set(partial, path, value);\n model.dirtyTracker.mergeChanges(partial);\n\n return model;\n}\n\nexport function hasField(model: Model, field: string): boolean {\n return get(model.data, field, MISSING_VALUE as any) !== MISSING_VALUE;\n}\n\nexport function incrementField(model: Model, field: string, amount?: number): Model {\n const value = getFieldValue(model, field, 0) as number;\n const incrementedValue = value + (amount ?? 1);\n return setFieldValue(model, field, incrementedValue);\n}\n\nexport function decrementField(model: Model, field: string, amount?: number): Model {\n const value = getFieldValue(model, field, 0) as number;\n const decrementedValue = value - (amount ?? 1);\n return setFieldValue(model, field, decrementedValue);\n}\n\nexport function unsetFields(model: Model, ...fields: string[]): Model {\n model.data = unset(model.data, fields);\n model.dirtyTracker.unset(fields);\n\n return model;\n}\n\n/**\n * Identity columns that a mass-assignment payload may never carry into an\n * already-persisted model. `merge()` is the method request bodies reach\n * (`model.merge(req.body)` / `save({ merge })`), and the primary key is what\n * the UPDATE filter is built from — letting it through means a payload of\n * `{ id: \"<victim-id>\" }` rewrites which document the save targets. Both the\n * configured primary key and the two framework identity columns are dropped,\n * because `id` and `_id` are managed by the writer/driver either way.\n *\n * A NEW model is untouched: creating with an explicit id is a legitimate flow,\n * and the writer itself merges the driver-returned document (`_id`, defaults)\n * back onto the instance before it is marked persisted.\n */\nfunction stripIdentityColumns(\n model: Model,\n values: Record<string, unknown>,\n): Record<string, unknown> {\n if (model.isNew) return values;\n\n const identityColumns = new Set([model.getPrimaryKey(), \"id\", \"_id\"]);\n const blockedKeys = Object.keys(values).filter(key => identityColumns.has(key));\n\n if (blockedKeys.length === 0) return values;\n\n // Copy — the caller's object (often the request body itself) is not ours to mutate.\n const safeValues = { ...values };\n for (const key of blockedKeys) {\n delete safeValues[key];\n }\n\n return safeValues;\n}\n\nexport function mergeFields(model: Model, values: Record<string, unknown>): Model {\n return mergeDriverFields(model, stripIdentityColumns(model, values));\n}\n\n/**\n * Merge a document the DRIVER produced (generated `_id`, `RETURNING *`, DB\n * defaults) back onto the model, identity columns included.\n *\n * Framework-internal counterpart of {@link mergeFields}: the write pipeline is\n * the one caller allowed to set identity columns on an already-persisted\n * instance, because the values come from the database rather than from a\n * request. Never route caller-supplied data through this.\n */\nexport function mergeDriverFields(model: Model, values: Record<string, unknown>): Model {\n model.data = merge(model.data, values) as any;\n model.dirtyTracker.mergeChanges(values);\n return model;\n}\n\nexport function getOnlyFields(model: Model, fields: string[]): Record<string, unknown> {\n return only(model.data, fields);\n}\n\nexport function getStringField(model: Model, key: string, defaultValue?: string): string | undefined {\n return getFieldValue(model, key, defaultValue) as string | undefined;\n}\n\nexport function getNumberField(model: Model, key: string, defaultValue?: number): number | undefined {\n return getFieldValue(model, key, defaultValue) as number | undefined;\n}\n\nexport function getBooleanField(model: Model, key: string, defaultValue?: boolean): boolean | undefined {\n return getFieldValue(model, key, defaultValue) as boolean | undefined;\n}\n"],"mappings":";;;;;;;AAOA,MAAM,gBAAgB,OAAO,SAAS;AAEtC,SAAgB,cAAc,OAAc,OAAe,cAA6B;CACtF,OAAO,IAAI,MAAM,MAAM,OAAO,YAAY;AAC5C;AAEA,SAAgB,cAAc,OAAc,OAAe,OAAuB;CAChF,MAAM,OAAO,OAAO,KAAK;CACzB,IAAI,MAAM,MAAM,MAAM,KAAK;CAE3B,MAAM,UAAmC,CAAC;CAC1C,IAAI,SAAS,MAAM,KAAK;CACxB,MAAM,aAAa,aAAa,OAAO;CAEvC,OAAO;AACT;AAEA,SAAgB,SAAS,OAAc,OAAwB;CAC7D,OAAO,IAAI,MAAM,MAAM,OAAO,aAAoB,MAAM;AAC1D;AAEA,SAAgB,eAAe,OAAc,OAAe,QAAwB;CAGlF,OAAO,cAAc,OAAO,OAFd,cAAc,OAAO,OAAO,CACb,KAAK,UAAU,EACO;AACrD;AAEA,SAAgB,eAAe,OAAc,OAAe,QAAwB;CAGlF,OAAO,cAAc,OAAO,OAFd,cAAc,OAAO,OAAO,CACb,KAAK,UAAU,EACO;AACrD;AAEA,SAAgB,YAAY,OAAc,GAAG,QAAyB;CACpE,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM;CACrC,MAAM,aAAa,MAAM,MAAM;CAE/B,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,qBACP,OACA,QACyB;CACzB,IAAI,MAAM,OAAO,OAAO;CAExB,MAAM,kBAAkB,IAAI,IAAI;EAAC,MAAM,cAAc;EAAG;EAAM;CAAK,CAAC;CACpE,MAAM,cAAc,OAAO,KAAK,MAAM,CAAC,CAAC,QAAO,QAAO,gBAAgB,IAAI,GAAG,CAAC;CAE9E,IAAI,YAAY,WAAW,GAAG,OAAO;CAGrC,MAAM,aAAa,EAAE,GAAG,OAAO;CAC/B,KAAK,MAAM,OAAO,aAChB,OAAO,WAAW;CAGpB,OAAO;AACT;AAEA,SAAgB,YAAY,OAAc,QAAwC;CAChF,OAAO,kBAAkB,OAAO,qBAAqB,OAAO,MAAM,CAAC;AACrE;;;;;;;;;;AAWA,SAAgB,kBAAkB,OAAc,QAAwC;CACtF,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM;CACrC,MAAM,aAAa,aAAa,MAAM;CACtC,OAAO;AACT;AAEA,SAAgB,cAAc,OAAc,QAA2C;CACrF,OAAO,KAAK,MAAM,MAAM,MAAM;AAChC;AAEA,SAAgB,eAAe,OAAc,KAAa,cAA2C;CACnG,OAAO,cAAc,OAAO,KAAK,YAAY;AAC/C;AAEA,SAAgB,eAAe,OAAc,KAAa,cAA2C;CACnG,OAAO,cAAc,OAAO,KAAK,YAAY;AAC/C;AAEA,SAAgB,gBAAgB,OAAc,KAAa,cAA6C;CACtG,OAAO,cAAc,OAAO,KAAK,YAAY;AAC/C"}
1
+ {"version":3,"file":"accessor-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/accessor-methods.ts"],"sourcesContent":["import { get, merge, only, set, unset } from \"@mongez/reinforcements\";\nimport type { Model } from \"../model\";\n\n/**\n * Sentinel symbol to distinguish a genuinely missing field from a field\n * whose value is `undefined`. Encapsulated here — callers use `hasField()`.\n */\nconst MISSING_VALUE = Symbol(\"missing\");\n\nexport function getFieldValue(model: Model, field: string, defaultValue?: unknown): any {\n return get(model.data, field, defaultValue);\n}\n\nexport function setFieldValue(model: Model, field: string, value: unknown): Model {\n const path = String(field);\n set(model.data, path, value);\n\n model.dirtyTracker.setAtPath(path, value);\n\n return model;\n}\n\nexport function hasField(model: Model, field: string): boolean {\n return get(model.data, field, MISSING_VALUE as any) !== MISSING_VALUE;\n}\n\nexport function incrementField(model: Model, field: string, amount?: number): Model {\n const value = getFieldValue(model, field, 0) as number;\n const incrementedValue = value + (amount ?? 1);\n return setFieldValue(model, field, incrementedValue);\n}\n\nexport function decrementField(model: Model, field: string, amount?: number): Model {\n const value = getFieldValue(model, field, 0) as number;\n const decrementedValue = value - (amount ?? 1);\n return setFieldValue(model, field, decrementedValue);\n}\n\nexport function unsetFields(model: Model, ...fields: string[]): Model {\n model.data = unset(model.data, fields);\n model.dirtyTracker.unset(fields);\n\n return model;\n}\n\n/**\n * Identity columns that a mass-assignment payload may never carry into an\n * already-persisted model. `merge()` is the method request bodies reach\n * (`model.merge(req.body)` / `save({ merge })`), and the primary key is what\n * the UPDATE filter is built from — letting it through means a payload of\n * `{ id: \"<victim-id>\" }` rewrites which document the save targets. Both the\n * configured primary key and the two framework identity columns are dropped,\n * because `id` and `_id` are managed by the writer/driver either way.\n *\n * A NEW model is untouched: creating with an explicit id is a legitimate flow,\n * and the writer itself merges the driver-returned document (`_id`, defaults)\n * back onto the instance before it is marked persisted.\n */\nfunction stripIdentityColumns(\n model: Model,\n values: Record<string, unknown>,\n): Record<string, unknown> {\n if (model.isNew) return values;\n\n const identityColumns = new Set([model.getPrimaryKey(), \"id\", \"_id\"]);\n const blockedKeys = Object.keys(values).filter(key => identityColumns.has(key));\n\n if (blockedKeys.length === 0) return values;\n\n // Copy — the caller's object (often the request body itself) is not ours to mutate.\n const safeValues = { ...values };\n for (const key of blockedKeys) {\n delete safeValues[key];\n }\n\n return safeValues;\n}\n\nexport function mergeFields(model: Model, values: Record<string, unknown>): Model {\n return mergeDriverFields(model, stripIdentityColumns(model, values));\n}\n\n/**\n * Merge a document the DRIVER produced (generated `_id`, `RETURNING *`, DB\n * defaults) back onto the model, identity columns included.\n *\n * Framework-internal counterpart of {@link mergeFields}: the write pipeline is\n * the one caller allowed to set identity columns on an already-persisted\n * instance, because the values come from the database rather than from a\n * request. Never route caller-supplied data through this.\n */\nexport function mergeDriverFields(model: Model, values: Record<string, unknown>): Model {\n model.data = merge(model.data, values) as any;\n model.dirtyTracker.mergeChanges(values);\n return model;\n}\n\nexport function getOnlyFields(model: Model, fields: string[]): Record<string, unknown> {\n return only(model.data, fields);\n}\n\nexport function getStringField(model: Model, key: string, defaultValue?: string): string | undefined {\n return getFieldValue(model, key, defaultValue) as string | undefined;\n}\n\nexport function getNumberField(model: Model, key: string, defaultValue?: number): number | undefined {\n return getFieldValue(model, key, defaultValue) as number | undefined;\n}\n\nexport function getBooleanField(model: Model, key: string, defaultValue?: boolean): boolean | undefined {\n return getFieldValue(model, key, defaultValue) as boolean | undefined;\n}\n"],"mappings":";;;;;;;AAOA,MAAM,gBAAgB,OAAO,SAAS;AAEtC,SAAgB,cAAc,OAAc,OAAe,cAA6B;CACtF,OAAO,IAAI,MAAM,MAAM,OAAO,YAAY;AAC5C;AAEA,SAAgB,cAAc,OAAc,OAAe,OAAuB;CAChF,MAAM,OAAO,OAAO,KAAK;CACzB,IAAI,MAAM,MAAM,MAAM,KAAK;CAE3B,MAAM,aAAa,UAAU,MAAM,KAAK;CAExC,OAAO;AACT;AAEA,SAAgB,SAAS,OAAc,OAAwB;CAC7D,OAAO,IAAI,MAAM,MAAM,OAAO,aAAoB,MAAM;AAC1D;AAEA,SAAgB,eAAe,OAAc,OAAe,QAAwB;CAGlF,OAAO,cAAc,OAAO,OAFd,cAAc,OAAO,OAAO,CACb,KAAK,UAAU,EACO;AACrD;AAEA,SAAgB,eAAe,OAAc,OAAe,QAAwB;CAGlF,OAAO,cAAc,OAAO,OAFd,cAAc,OAAO,OAAO,CACb,KAAK,UAAU,EACO;AACrD;AAEA,SAAgB,YAAY,OAAc,GAAG,QAAyB;CACpE,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM;CACrC,MAAM,aAAa,MAAM,MAAM;CAE/B,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,qBACP,OACA,QACyB;CACzB,IAAI,MAAM,OAAO,OAAO;CAExB,MAAM,kBAAkB,IAAI,IAAI;EAAC,MAAM,cAAc;EAAG;EAAM;CAAK,CAAC;CACpE,MAAM,cAAc,OAAO,KAAK,MAAM,CAAC,CAAC,QAAO,QAAO,gBAAgB,IAAI,GAAG,CAAC;CAE9E,IAAI,YAAY,WAAW,GAAG,OAAO;CAGrC,MAAM,aAAa,EAAE,GAAG,OAAO;CAC/B,KAAK,MAAM,OAAO,aAChB,OAAO,WAAW;CAGpB,OAAO;AACT;AAEA,SAAgB,YAAY,OAAc,QAAwC;CAChF,OAAO,kBAAkB,OAAO,qBAAqB,OAAO,MAAM,CAAC;AACrE;;;;;;;;;;AAWA,SAAgB,kBAAkB,OAAc,QAAwC;CACtF,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM;CACrC,MAAM,aAAa,aAAa,MAAM;CACtC,OAAO;AACT;AAEA,SAAgB,cAAc,OAAc,QAA2C;CACrF,OAAO,KAAK,MAAM,MAAM,MAAM;AAChC;AAEA,SAAgB,eAAe,OAAc,KAAa,cAA2C;CACnG,OAAO,cAAc,OAAO,KAAK,YAAY;AAC/C;AAEA,SAAgB,eAAe,OAAc,KAAa,cAA2C;CACnG,OAAO,cAAc,OAAO,KAAK,YAAY;AAC/C;AAEA,SAAgB,gBAAgB,OAAc,KAAa,cAA6C;CACtG,OAAO,cAAc,OAAO,KAAK,YAAY;AAC/C"}
@@ -6,10 +6,14 @@ async function destroyModel(model, options) {
6
6
  return new DatabaseRemover(model).destroy(options);
7
7
  }
8
8
  async function deleteRecords(ModelClass, filter) {
9
- return ModelClass.getDriver().deleteMany(ModelClass.table, filter ? sanitizeFilter(filter) : filter);
9
+ const sanitized = filter ? sanitizeFilter(filter) : void 0;
10
+ if (!sanitized || Object.keys(sanitized).length === 0) throw new Error(`${ModelClass.name}.delete() requires a filter: pass an explicit filter, or use ${ModelClass.name}.deleteAll() / ${ModelClass.name}.query().delete() to delete every row.`);
11
+ return ModelClass.query().where(sanitized).delete();
10
12
  }
11
13
  async function deleteOneRecord(ModelClass, filter) {
12
- return ModelClass.getDriver().delete(ModelClass.table, filter ? sanitizeFilter(filter) : filter);
14
+ const query = ModelClass.query();
15
+ if (filter && Object.keys(filter).length > 0) query.where(sanitizeFilter(filter));
16
+ return query.deleteOne();
13
17
  }
14
18
 
15
19
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"delete-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/delete-methods.ts"],"sourcesContent":["import { DatabaseRemover } from \"../../remover/database-remover\";\r\nimport type { DeleteStrategy } from \"../../types\";\r\nimport type { RemoverResult } from \"../../contracts\";\r\nimport { sanitizeFilter } from \"../../utils/sanitize-filter\";\r\nimport type { ChildModel, Model } from \"../model\";\r\n\r\nexport async function destroyModel(\r\n model: Model,\r\n options?: { strategy?: DeleteStrategy; skipEvents?: boolean },\r\n): Promise<RemoverResult> {\r\n const remover = new DatabaseRemover(model);\r\n return remover.destroy(options);\r\n}\r\n\r\nexport async function deleteRecords(\r\n ModelClass: ChildModel<any>,\r\n filter?: Record<string, unknown>,\r\n): Promise<number> {\r\n return ModelClass.getDriver().deleteMany(ModelClass.table, filter ? sanitizeFilter(filter) : filter);\r\n}\r\n\r\nexport async function deleteOneRecord(\r\n ModelClass: ChildModel<any>,\r\n filter?: Record<string, unknown>,\r\n): Promise<number> {\r\n return ModelClass.getDriver().delete(ModelClass.table, filter ? sanitizeFilter(filter) : filter);\r\n}\r\n"],"mappings":";;;;AAMA,eAAsB,aACpB,OACA,SACwB;CAExB,OAAO,IADa,gBAAgB,KACvB,CAAC,CAAC,QAAQ,OAAO;AAChC;AAEA,eAAsB,cACpB,YACA,QACiB;CACjB,OAAO,WAAW,UAAU,CAAC,CAAC,WAAW,WAAW,OAAO,SAAS,eAAe,MAAM,IAAI,MAAM;AACrG;AAEA,eAAsB,gBACpB,YACA,QACiB;CACjB,OAAO,WAAW,UAAU,CAAC,CAAC,OAAO,WAAW,OAAO,SAAS,eAAe,MAAM,IAAI,MAAM;AACjG"}
1
+ {"version":3,"file":"delete-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/delete-methods.ts"],"sourcesContent":["import { DatabaseRemover } from \"../../remover/database-remover\";\r\nimport type { DeleteStrategy } from \"../../types\";\r\nimport type { RemoverResult } from \"../../contracts\";\r\nimport { sanitizeFilter } from \"../../utils/sanitize-filter\";\r\nimport type { ChildModel, Model } from \"../model\";\r\n\r\nexport async function destroyModel(\r\n model: Model,\r\n options?: { strategy?: DeleteStrategy; skipEvents?: boolean },\r\n): Promise<RemoverResult> {\r\n const remover = new DatabaseRemover(model);\r\n return remover.destroy(options);\r\n}\r\n\r\nexport async function deleteRecords(\r\n ModelClass: ChildModel<any>,\r\n filter?: Record<string, unknown>,\r\n): Promise<number> {\r\n const sanitized = filter ? sanitizeFilter(filter) : undefined;\r\n\r\n if (!sanitized || Object.keys(sanitized).length === 0) {\r\n throw new Error(\r\n `${ModelClass.name}.delete() requires a filter: pass an explicit filter, or use ${ModelClass.name}.deleteAll() / ${ModelClass.name}.query().delete() to delete every row.`,\r\n );\r\n }\r\n\r\n // Through the query builder so global scopes (tenant, soft delete) apply.\r\n return ModelClass.query().where(sanitized).delete();\r\n}\r\n\r\n/** Delete every row visible to the model's global scopes. Explicit on purpose. */\r\nexport async function deleteAllRecords(ModelClass: ChildModel<any>): Promise<number> {\r\n return ModelClass.query().delete();\r\n}\r\n\r\nexport async function deleteOneRecord(\r\n ModelClass: ChildModel<any>,\r\n filter?: Record<string, unknown>,\r\n): Promise<number> {\r\n const query = ModelClass.query();\r\n\r\n if (filter && Object.keys(filter).length > 0) {\r\n query.where(sanitizeFilter(filter));\r\n }\r\n\r\n return query.deleteOne();\r\n}\r\n"],"mappings":";;;;AAMA,eAAsB,aACpB,OACA,SACwB;CAExB,OAAO,IADa,gBAAgB,KACvB,CAAC,CAAC,QAAQ,OAAO;AAChC;AAEA,eAAsB,cACpB,YACA,QACiB;CACjB,MAAM,YAAY,SAAS,eAAe,MAAM,IAAI;CAEpD,IAAI,CAAC,aAAa,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,GAClD,MAAM,IAAI,MACR,GAAG,WAAW,KAAK,+DAA+D,WAAW,KAAK,iBAAiB,WAAW,KAAK,uCACrI;CAIF,OAAO,WAAW,MAAM,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,OAAO;AACpD;AAOA,eAAsB,gBACpB,YACA,QACiB;CACjB,MAAM,QAAQ,WAAW,MAAM;CAE/B,IAAI,UAAU,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GACzC,MAAM,MAAM,eAAe,MAAM,CAAC;CAGpC,OAAO,MAAM,UAAU;AACzB"}
@@ -1,8 +1,22 @@
1
1
  import { RelationHydrator } from "../../relations/relation-hydrator.mjs";
2
+ import { clone } from "@mongez/reinforcements";
3
+ import { DateValidator } from "@warlock.js/seal";
2
4
 
3
5
  //#region ../cascade/src/model/methods/hydration-methods.ts
6
+ /**
7
+ * Drivers re-inflate any ISO-looking string into a Date. Only fields the model
8
+ * schema declares as dates may be converted, so put every other string back.
9
+ */
10
+ function restoreUndeclaredDateStrings(ModelClass, original, data) {
11
+ const shape = ModelClass.schema?.schema ?? {};
12
+ for (const [key, value] of Object.entries(original)) if (data[key] instanceof Date && !(shape[key] instanceof DateValidator)) data[key] = value;
13
+ }
4
14
  function hydrateModel(ModelClass, data) {
5
- const model = new ModelClass(ModelClass.getDriver().deserialize(data));
15
+ const strings = {};
16
+ for (const [key, value] of Object.entries(data)) if (typeof value === "string") strings[key] = value;
17
+ const deserialized = ModelClass.getDriver().deserialize(data);
18
+ restoreUndeclaredDateStrings(ModelClass, strings, deserialized);
19
+ const model = new ModelClass(deserialized);
6
20
  model.isNew = false;
7
21
  return model;
8
22
  }
@@ -26,7 +40,7 @@ function serializeModel(model) {
26
40
  return model.constructor.getDataSource().driver.serialize(model.data);
27
41
  }
28
42
  function cloneModel(model) {
29
- const clonedData = JSON.parse(JSON.stringify(model.data));
43
+ const clonedData = clone(model.data);
30
44
  const clonedModel = new (model.self())(clonedData);
31
45
  clonedModel.isNew = model.isNew;
32
46
  deepFreezeObject(clonedModel.data);
@@ -1 +1 @@
1
- {"version":3,"file":"hydration-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/hydration-methods.ts"],"sourcesContent":["import { RelationHydrator, type ModelSnapshot, type SerializedRelation } from \"../../relations/relation-hydrator\";\nimport type { ChildModel, Model } from \"../model\";\n\nexport function hydrateModel<TModel extends Model = Model>(\n ModelClass: ChildModel<TModel>,\n data: Record<string, unknown>,\n): TModel {\n const model = new ModelClass(ModelClass.getDriver().deserialize(data));\n model.isNew = false;\n return model;\n}\n\nexport function modelFromSnapshot<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n snapshot: ModelSnapshot,\n): TModel {\n const model = ModelClass.hydrate(snapshot.data) as TModel;\n RelationHydrator.hydrate(model, ModelClass.relations, snapshot.relations);\n return model;\n}\n\nexport function modelToSnapshot(model: Model): ModelSnapshot {\n const driver = (model.constructor as typeof Model).getDataSource().driver;\n const relations: Record<string, SerializedRelation> = {};\n\n for (const [name, related] of model.loadedRelations) {\n if (related === null) {\n relations[name] = null;\n } else if (Array.isArray(related)) {\n relations[name] = related.map((m) => (m instanceof Object && typeof m.toSnapshot === \"function\" ? m.toSnapshot() : m));\n } else if (related instanceof Object && typeof related.toSnapshot === \"function\") {\n relations[name] = related.toSnapshot();\n }\n }\n\n return {\n data: driver.serialize({ ...model.data }) as Record<string, unknown>,\n relations,\n };\n}\n\nexport function serializeModel(model: Model) {\n return (model.constructor as typeof Model).getDataSource().driver.serialize(model.data);\n}\n\nexport function cloneModel<TModel extends Model>(model: TModel): TModel {\n const clonedData = JSON.parse(JSON.stringify(model.data));\n const ModelClass = model.self();\n const clonedModel = new ModelClass(clonedData) as TModel;\n\n clonedModel.isNew = model.isNew;\n deepFreezeObject(clonedModel.data);\n clonedModel.dirtyTracker.reset();\n\n return clonedModel;\n}\n\nexport function deepFreezeObject<T>(obj: T): T {\n Object.freeze(obj);\n\n Object.getOwnPropertyNames(obj).forEach((prop) => {\n const value = (obj as any)[prop];\n if (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n !Object.isFrozen(value)\n ) {\n deepFreezeObject(value);\n }\n });\n\n return obj;\n}\n\nexport function replaceModelData<TModel extends Model>(\n model: TModel,\n data: Record<string, unknown>,\n): void {\n model.data = data as any;\n model.dirtyTracker.replaceCurrentData(data);\n}\n"],"mappings":";;;AAGA,SAAgB,aACd,YACA,MACQ;CACR,MAAM,QAAQ,IAAI,WAAW,WAAW,UAAU,CAAC,CAAC,YAAY,IAAI,CAAC;CACrE,MAAM,QAAQ;CACd,OAAO;AACT;AAEA,SAAgB,kBACd,YACA,UACQ;CACR,MAAM,QAAQ,WAAW,QAAQ,SAAS,IAAI;CAC9C,iBAAiB,QAAQ,OAAO,WAAW,WAAW,SAAS,SAAS;CACxE,OAAO;AACT;AAEA,SAAgB,gBAAgB,OAA6B;CAC3D,MAAM,SAAU,MAAM,YAA6B,cAAc,CAAC,CAAC;CACnE,MAAM,YAAgD,CAAC;CAEvD,KAAK,MAAM,CAAC,MAAM,YAAY,MAAM,iBAClC,IAAI,YAAY,MACd,UAAU,QAAQ;MACb,IAAI,MAAM,QAAQ,OAAO,GAC9B,UAAU,QAAQ,QAAQ,KAAK,MAAO,aAAa,UAAU,OAAO,EAAE,eAAe,aAAa,EAAE,WAAW,IAAI,CAAE;MAChH,IAAI,mBAAmB,UAAU,OAAO,QAAQ,eAAe,YACpE,UAAU,QAAQ,QAAQ,WAAW;CAIzC,OAAO;EACL,MAAM,OAAO,UAAU,EAAE,GAAG,MAAM,KAAK,CAAC;EACxC;CACF;AACF;AAEA,SAAgB,eAAe,OAAc;CAC3C,OAAQ,MAAM,YAA6B,cAAc,CAAC,CAAC,OAAO,UAAU,MAAM,IAAI;AACxF;AAEA,SAAgB,WAAiC,OAAuB;CACtE,MAAM,aAAa,KAAK,MAAM,KAAK,UAAU,MAAM,IAAI,CAAC;CAExD,MAAM,cAAc,KADD,MAAM,KACQ,GAAE,UAAU;CAE7C,YAAY,QAAQ,MAAM;CAC1B,iBAAiB,YAAY,IAAI;CACjC,YAAY,aAAa,MAAM;CAE/B,OAAO;AACT;AAEA,SAAgB,iBAAoB,KAAW;CAC7C,OAAO,OAAO,GAAG;CAEjB,OAAO,oBAAoB,GAAG,CAAC,CAAC,SAAS,SAAS;EAChD,MAAM,QAAS,IAAY;EAC3B,IACE,UAAU,SACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,CAAC,OAAO,SAAS,KAAK,GAEtB,iBAAiB,KAAK;CAE1B,CAAC;CAED,OAAO;AACT;AAEA,SAAgB,iBACd,OACA,MACM;CACN,MAAM,OAAO;CACb,MAAM,aAAa,mBAAmB,IAAI;AAC5C"}
1
+ {"version":3,"file":"hydration-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/hydration-methods.ts"],"sourcesContent":["import { clone } from \"@mongez/reinforcements\";\nimport { DateValidator } from \"@warlock.js/seal\";\nimport { RelationHydrator, type ModelSnapshot, type SerializedRelation } from \"../../relations/relation-hydrator\";\nimport type { ChildModel, Model } from \"../model\";\n\n/**\n * Drivers re-inflate any ISO-looking string into a Date. Only fields the model\n * schema declares as dates may be converted, so put every other string back.\n */\nfunction restoreUndeclaredDateStrings<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n original: Record<string, string>,\n data: Record<string, unknown>,\n): void {\n const shape = ModelClass.schema?.schema ?? {};\n\n for (const [key, value] of Object.entries(original)) {\n if (data[key] instanceof Date && !(shape[key] instanceof DateValidator)) {\n data[key] = value;\n }\n }\n}\n\nexport function hydrateModel<TModel extends Model = Model>(\n ModelClass: ChildModel<TModel>,\n data: Record<string, unknown>,\n): TModel {\n const strings: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\") strings[key] = value;\n }\n\n const deserialized = ModelClass.getDriver().deserialize(data);\n restoreUndeclaredDateStrings(ModelClass, strings, deserialized);\n\n const model = new ModelClass(deserialized);\n model.isNew = false;\n return model;\n}\n\nexport function modelFromSnapshot<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n snapshot: ModelSnapshot,\n): TModel {\n const model = ModelClass.hydrate(snapshot.data) as TModel;\n RelationHydrator.hydrate(model, ModelClass.relations, snapshot.relations);\n return model;\n}\n\nexport function modelToSnapshot(model: Model): ModelSnapshot {\n const driver = (model.constructor as typeof Model).getDataSource().driver;\n const relations: Record<string, SerializedRelation> = {};\n\n for (const [name, related] of model.loadedRelations) {\n if (related === null) {\n relations[name] = null;\n } else if (Array.isArray(related)) {\n relations[name] = related.map((m) => (m instanceof Object && typeof m.toSnapshot === \"function\" ? m.toSnapshot() : m));\n } else if (related instanceof Object && typeof related.toSnapshot === \"function\") {\n relations[name] = related.toSnapshot();\n }\n }\n\n return {\n data: driver.serialize({ ...model.data }) as Record<string, unknown>,\n relations,\n };\n}\n\nexport function serializeModel(model: Model) {\n return (model.constructor as typeof Model).getDataSource().driver.serialize(model.data);\n}\n\nexport function cloneModel<TModel extends Model>(model: TModel): TModel {\n const clonedData = clone(model.data);\n const ModelClass = model.self();\n const clonedModel = new ModelClass(clonedData) as TModel;\n\n clonedModel.isNew = model.isNew;\n deepFreezeObject(clonedModel.data);\n clonedModel.dirtyTracker.reset();\n\n return clonedModel;\n}\n\nexport function deepFreezeObject<T>(obj: T): T {\n Object.freeze(obj);\n\n Object.getOwnPropertyNames(obj).forEach((prop) => {\n const value = (obj as any)[prop];\n if (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n !Object.isFrozen(value)\n ) {\n deepFreezeObject(value);\n }\n });\n\n return obj;\n}\n\nexport function replaceModelData<TModel extends Model>(\n model: TModel,\n data: Record<string, unknown>,\n): void {\n model.data = data as any;\n model.dirtyTracker.replaceCurrentData(data);\n}\n"],"mappings":";;;;;;;;;AASA,SAAS,6BACP,YACA,UACA,MACM;CACN,MAAM,QAAQ,WAAW,QAAQ,UAAU,CAAC;CAE5C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,GAChD,IAAI,KAAK,gBAAgB,QAAQ,EAAE,MAAM,gBAAgB,gBACvD,KAAK,OAAO;AAGlB;AAEA,SAAgB,aACd,YACA,MACQ;CACR,MAAM,UAAkC,CAAC;CAEzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAC5C,IAAI,OAAO,UAAU,UAAU,QAAQ,OAAO;CAGhD,MAAM,eAAe,WAAW,UAAU,CAAC,CAAC,YAAY,IAAI;CAC5D,6BAA6B,YAAY,SAAS,YAAY;CAE9D,MAAM,QAAQ,IAAI,WAAW,YAAY;CACzC,MAAM,QAAQ;CACd,OAAO;AACT;AAEA,SAAgB,kBACd,YACA,UACQ;CACR,MAAM,QAAQ,WAAW,QAAQ,SAAS,IAAI;CAC9C,iBAAiB,QAAQ,OAAO,WAAW,WAAW,SAAS,SAAS;CACxE,OAAO;AACT;AAEA,SAAgB,gBAAgB,OAA6B;CAC3D,MAAM,SAAU,MAAM,YAA6B,cAAc,CAAC,CAAC;CACnE,MAAM,YAAgD,CAAC;CAEvD,KAAK,MAAM,CAAC,MAAM,YAAY,MAAM,iBAClC,IAAI,YAAY,MACd,UAAU,QAAQ;MACb,IAAI,MAAM,QAAQ,OAAO,GAC9B,UAAU,QAAQ,QAAQ,KAAK,MAAO,aAAa,UAAU,OAAO,EAAE,eAAe,aAAa,EAAE,WAAW,IAAI,CAAE;MAChH,IAAI,mBAAmB,UAAU,OAAO,QAAQ,eAAe,YACpE,UAAU,QAAQ,QAAQ,WAAW;CAIzC,OAAO;EACL,MAAM,OAAO,UAAU,EAAE,GAAG,MAAM,KAAK,CAAC;EACxC;CACF;AACF;AAEA,SAAgB,eAAe,OAAc;CAC3C,OAAQ,MAAM,YAA6B,cAAc,CAAC,CAAC,OAAO,UAAU,MAAM,IAAI;AACxF;AAEA,SAAgB,WAAiC,OAAuB;CACtE,MAAM,aAAa,MAAM,MAAM,IAAI;CAEnC,MAAM,cAAc,KADD,MAAM,KACQ,GAAE,UAAU;CAE7C,YAAY,QAAQ,MAAM;CAC1B,iBAAiB,YAAY,IAAI;CACjC,YAAY,aAAa,MAAM;CAE/B,OAAO;AACT;AAEA,SAAgB,iBAAoB,KAAW;CAC7C,OAAO,OAAO,GAAG;CAEjB,OAAO,oBAAoB,GAAG,CAAC,CAAC,SAAS,SAAS;EAChD,MAAM,QAAS,IAAY;EAC3B,IACE,UAAU,SACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,CAAC,OAAO,SAAS,KAAK,GAEtB,iBAAiB,KAAK;CAE1B,CAAC;CAED,OAAO;AACT;AAEA,SAAgB,iBACd,OACA,MACM;CACN,MAAM,OAAO;CACb,MAAM,aAAa,mBAAmB,IAAI;AAC5C"}
@@ -1,10 +1,11 @@
1
1
  import { globalModelEvents } from "../../events/model-events.mjs";
2
+ import { emitStaticEvent } from "./static-event-methods.mjs";
2
3
 
3
4
  //#region ../cascade/src/model/methods/instance-event-methods.ts
4
5
  async function emitModelEvent(model, event, context) {
5
6
  const ctor = model.constructor;
6
7
  await model.events.emit(event, model, context);
7
- await ctor.events().emit(event, model, context);
8
+ await emitStaticEvent(ctor, event, model, context);
8
9
  await globalModelEvents.emit(event, model, context);
9
10
  }
10
11
  function onModelEvent(model, event, listener) {
@@ -1 +1 @@
1
- {"version":3,"file":"instance-event-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/instance-event-methods.ts"],"sourcesContent":["import {\n globalModelEvents,\n type ModelEventListener,\n type ModelEventName,\n} from \"../../events/model-events\";\nimport type { Model } from \"../model\";\n\nexport async function emitModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n context?: TContext,\n): Promise<void> {\n const ctor = model.constructor as any;\n await model.events.emit(event, model, context as TContext);\n await ctor.events().emit(event, model, context as TContext);\n await globalModelEvents.emit(event, model, context as TContext);\n}\n\nexport function onModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n listener: ModelEventListener<any, TContext>,\n): () => void {\n return model.events.on(event, listener);\n}\n\nexport function onceModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n listener: ModelEventListener<any, TContext>,\n): () => void {\n return model.events.once(event, listener);\n}\n\nexport function offModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n listener: ModelEventListener<any, TContext>,\n): void {\n model.events.off(event, listener);\n}\n"],"mappings":";;;AAOA,eAAsB,eACpB,OACA,OACA,SACe;CACf,MAAM,OAAO,MAAM;CACnB,MAAM,MAAM,OAAO,KAAK,OAAO,OAAO,OAAmB;CACzD,MAAM,KAAK,OAAO,CAAC,CAAC,KAAK,OAAO,OAAO,OAAmB;CAC1D,MAAM,kBAAkB,KAAK,OAAO,OAAO,OAAmB;AAChE;AAEA,SAAgB,aACd,OACA,OACA,UACY;CACZ,OAAO,MAAM,OAAO,GAAG,OAAO,QAAQ;AACxC;AAEA,SAAgB,eACd,OACA,OACA,UACY;CACZ,OAAO,MAAM,OAAO,KAAK,OAAO,QAAQ;AAC1C;AAEA,SAAgB,cACd,OACA,OACA,UACM;CACN,MAAM,OAAO,IAAI,OAAO,QAAQ;AAClC"}
1
+ {"version":3,"file":"instance-event-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/instance-event-methods.ts"],"sourcesContent":["import {\n globalModelEvents,\n type ModelEventListener,\n type ModelEventName,\n} from \"../../events/model-events\";\nimport type { Model } from \"../model\";\nimport { emitStaticEvent } from \"./static-event-methods\";\n\nexport async function emitModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n context?: TContext,\n): Promise<void> {\n const ctor = model.constructor as any;\n await model.events.emit(event, model, context as TContext);\n await emitStaticEvent(ctor, event, model, context as TContext);\n await globalModelEvents.emit(event, model, context as TContext);\n}\n\nexport function onModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n listener: ModelEventListener<any, TContext>,\n): () => void {\n return model.events.on(event, listener);\n}\n\nexport function onceModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n listener: ModelEventListener<any, TContext>,\n): () => void {\n return model.events.once(event, listener);\n}\n\nexport function offModelEvent<TContext = unknown>(\n model: Model,\n event: ModelEventName,\n listener: ModelEventListener<any, TContext>,\n): void {\n model.events.off(event, listener);\n}\n"],"mappings":";;;;AAQA,eAAsB,eACpB,OACA,OACA,SACe;CACf,MAAM,OAAO,MAAM;CACnB,MAAM,MAAM,OAAO,KAAK,OAAO,OAAO,OAAmB;CACzD,MAAM,gBAAgB,MAAM,OAAO,OAAO,OAAmB;CAC7D,MAAM,kBAAkB,KAAK,OAAO,OAAO,OAAmB;AAChE;AAEA,SAAgB,aACd,OACA,OACA,UACY;CACZ,OAAO,MAAM,OAAO,GAAG,OAAO,QAAQ;AACxC;AAEA,SAAgB,eACd,OACA,OACA,UACY;CACZ,OAAO,MAAM,OAAO,KAAK,OAAO,QAAQ;AAC1C;AAEA,SAAgB,cACd,OACA,OACA,UACM;CACN,MAAM,OAAO,IAAI,OAAO,QAAQ;AAClC"}
@@ -1,34 +1,71 @@
1
1
  import { DatabaseWriter } from "../../writer/database-writer.mjs";
2
+ import { get, set } from "@mongez/reinforcements";
2
3
 
3
4
  //#region ../cascade/src/model/methods/meta-methods.ts
5
+ /**
6
+ * Whether a subclass (or an intermediate base below `Model`) set the static
7
+ * itself. The base `Model` initialises several statics (strictMode,
8
+ * autoGenerateId, incrementIdBy, deletedAtColumn), so comparing to
9
+ * `undefined` would never let data-source defaults apply.
10
+ */
11
+ function isExplicitStatic(ModelClass, key) {
12
+ let current = ModelClass;
13
+ while (current && Object.getPrototypeOf(current) !== Function.prototype) {
14
+ if (Object.prototype.hasOwnProperty.call(current, key) && current[key] !== void 0) return true;
15
+ current = Object.getPrototypeOf(current);
16
+ }
17
+ return false;
18
+ }
4
19
  function applyDefaultsToModel(ModelClass, defaults) {
5
- if (defaults.autoGenerateId !== void 0 && ModelClass.autoGenerateId === void 0) ModelClass.autoGenerateId = defaults.autoGenerateId;
6
- if (defaults.initialId !== void 0 && ModelClass.initialId === void 0) ModelClass.initialId = defaults.initialId;
7
- if (defaults.randomInitialId !== void 0 && ModelClass.randomInitialId === void 0) ModelClass.randomInitialId = defaults.randomInitialId;
8
- if (defaults.incrementIdBy !== void 0 && ModelClass.incrementIdBy === void 0) ModelClass.incrementIdBy = defaults.incrementIdBy;
9
- if (defaults.randomIncrement !== void 0 && ModelClass.randomIncrement === void 0) ModelClass.randomIncrement = defaults.randomIncrement;
10
- if (defaults.createdAtColumn !== void 0 && ModelClass.createdAtColumn === void 0) ModelClass.createdAtColumn = defaults.createdAtColumn;
11
- if (defaults.updatedAtColumn !== void 0 && ModelClass.updatedAtColumn === void 0) ModelClass.updatedAtColumn = defaults.updatedAtColumn;
12
- if (defaults.deleteStrategy !== void 0 && ModelClass.deleteStrategy === void 0) ModelClass.deleteStrategy = defaults.deleteStrategy;
13
- if (defaults.deletedAtColumn !== void 0 && ModelClass.deletedAtColumn === void 0) ModelClass.deletedAtColumn = defaults.deletedAtColumn;
14
- if (defaults.trashTable !== void 0 && ModelClass.trashTable === void 0) if (typeof defaults.trashTable === "function") ModelClass.trashTable = defaults.trashTable(ModelClass.table);
20
+ if (defaults.autoGenerateId !== void 0 && !isExplicitStatic(ModelClass, "autoGenerateId")) ModelClass.autoGenerateId = defaults.autoGenerateId;
21
+ if (defaults.initialId !== void 0 && !isExplicitStatic(ModelClass, "initialId")) ModelClass.initialId = defaults.initialId;
22
+ if (defaults.randomInitialId !== void 0 && !isExplicitStatic(ModelClass, "randomInitialId")) ModelClass.randomInitialId = defaults.randomInitialId;
23
+ if (defaults.incrementIdBy !== void 0 && !isExplicitStatic(ModelClass, "incrementIdBy")) ModelClass.incrementIdBy = defaults.incrementIdBy;
24
+ if (defaults.randomIncrement !== void 0 && !isExplicitStatic(ModelClass, "randomIncrement")) ModelClass.randomIncrement = defaults.randomIncrement;
25
+ if (defaults.createdAtColumn !== void 0 && !isExplicitStatic(ModelClass, "createdAtColumn")) ModelClass.createdAtColumn = defaults.createdAtColumn;
26
+ if (defaults.updatedAtColumn !== void 0 && !isExplicitStatic(ModelClass, "updatedAtColumn")) ModelClass.updatedAtColumn = defaults.updatedAtColumn;
27
+ if (defaults.deleteStrategy !== void 0 && !isExplicitStatic(ModelClass, "deleteStrategy")) ModelClass.deleteStrategy = defaults.deleteStrategy;
28
+ if (defaults.deletedAtColumn !== void 0 && !isExplicitStatic(ModelClass, "deletedAtColumn")) ModelClass.deletedAtColumn = defaults.deletedAtColumn;
29
+ if (defaults.trashTable !== void 0 && !isExplicitStatic(ModelClass, "trashTable")) if (typeof defaults.trashTable === "function") ModelClass.trashTable = defaults.trashTable(ModelClass.table);
15
30
  else ModelClass.trashTable = defaults.trashTable;
16
- if (defaults.strictMode !== void 0 && ModelClass.strictMode === void 0) ModelClass.strictMode = defaults.strictMode;
31
+ if (defaults.strictMode !== void 0 && !isExplicitStatic(ModelClass, "strictMode")) ModelClass.strictMode = defaults.strictMode;
17
32
  }
18
33
  async function generateModelNextId(model) {
19
34
  await new DatabaseWriter(model).generateNextId();
20
35
  return model.id;
21
36
  }
22
37
  async function performAtomicUpdate(model, operations) {
23
- return model.self().atomic({ id: model.id }, operations);
38
+ return model.self().atomic({ [model.self().primaryKey]: model.trustedPrimaryKey }, operations);
39
+ }
40
+ /**
41
+ * Apply an atomic delta to the local value without marking the field dirty.
42
+ * The database already holds the change, so a later `save()` must not send a
43
+ * stale `$set` for it. Other pending changes stay dirty.
44
+ */
45
+ function applyLocalDelta(model, field, delta) {
46
+ const tracker = model.dirtyTracker;
47
+ const fieldWasDirty = tracker.isDirty(field);
48
+ const value = get(model.data, field, 0) + delta;
49
+ set(model.data, field, value);
50
+ if (fieldWasDirty || tracker.getRemovedColumns().length > 0) {
51
+ tracker.setAtPath(field, value);
52
+ return;
53
+ }
54
+ const pending = tracker.getDirtyColumnsWithValues();
55
+ const baseline = structuredClone(model.data);
56
+ for (const [column, values] of Object.entries(pending)) set(baseline, column, values.oldValue);
57
+ tracker.reset(baseline);
58
+ for (const [column, values] of Object.entries(pending)) tracker.setAtPath(column, values.newValue);
24
59
  }
25
60
  async function performAtomicIncrement(model, field, amount = 1) {
26
- model.increment(field, amount);
27
- return performAtomicUpdate(model, { $inc: { [field]: amount } });
61
+ const result = await performAtomicUpdate(model, { $inc: { [field]: amount } });
62
+ applyLocalDelta(model, field, amount);
63
+ return result;
28
64
  }
29
65
  async function performAtomicDecrement(model, field, amount = 1) {
30
- model.decrement(field, amount);
31
- return performAtomicUpdate(model, { $inc: { [field]: -amount } });
66
+ const result = await performAtomicUpdate(model, { $inc: { [field]: -amount } });
67
+ applyLocalDelta(model, field, -amount);
68
+ return result;
32
69
  }
33
70
 
34
71
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"meta-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/meta-methods.ts"],"sourcesContent":["import { DatabaseWriter } from \"../../writer/database-writer\";\nimport type { Model } from \"../model\";\n\nexport function applyDefaultsToModel(ModelClass: any, defaults: any): void {\n // Only apply defaults if model doesn't have its own value\n\n // ============================================================================\n // ID Generation\n // ============================================================================\n if (defaults.autoGenerateId !== undefined && ModelClass.autoGenerateId === undefined) {\n ModelClass.autoGenerateId = defaults.autoGenerateId;\n }\n if (defaults.initialId !== undefined && ModelClass.initialId === undefined) {\n ModelClass.initialId = defaults.initialId;\n }\n if (defaults.randomInitialId !== undefined && ModelClass.randomInitialId === undefined) {\n ModelClass.randomInitialId = defaults.randomInitialId;\n }\n if (defaults.incrementIdBy !== undefined && ModelClass.incrementIdBy === undefined) {\n ModelClass.incrementIdBy = defaults.incrementIdBy;\n }\n if (defaults.randomIncrement !== undefined && ModelClass.randomIncrement === undefined) {\n ModelClass.randomIncrement = defaults.randomIncrement;\n }\n\n // ============================================================================\n // Timestamps\n // ============================================================================\n if (defaults.createdAtColumn !== undefined && ModelClass.createdAtColumn === undefined) {\n ModelClass.createdAtColumn = defaults.createdAtColumn;\n }\n\n if (defaults.updatedAtColumn !== undefined && ModelClass.updatedAtColumn === undefined) {\n ModelClass.updatedAtColumn = defaults.updatedAtColumn;\n }\n\n // ============================================================================\n // Deletion\n // ============================================================================\n if (defaults.deleteStrategy !== undefined && ModelClass.deleteStrategy === undefined) {\n ModelClass.deleteStrategy = defaults.deleteStrategy;\n }\n if (defaults.deletedAtColumn !== undefined && ModelClass.deletedAtColumn === undefined) {\n ModelClass.deletedAtColumn = defaults.deletedAtColumn;\n }\n if (defaults.trashTable !== undefined && ModelClass.trashTable === undefined) {\n // Handle function-based trash table\n if (typeof defaults.trashTable === \"function\") {\n ModelClass.trashTable = defaults.trashTable(ModelClass.table);\n } else {\n ModelClass.trashTable = defaults.trashTable;\n }\n }\n\n // ============================================================================\n // Validation\n // ============================================================================\n if (defaults.strictMode !== undefined && ModelClass.strictMode === undefined) {\n ModelClass.strictMode = defaults.strictMode;\n }\n}\n\nexport async function generateModelNextId(model: Model): Promise<number | string> {\n const writer = new DatabaseWriter(model);\n await writer.generateNextId();\n return model.id!;\n}\n\nexport async function performAtomicUpdate(\n model: Model,\n operations: Record<string, unknown>,\n): Promise<number> {\n return model.self().atomic({ id: model.id! }, operations);\n}\n\nexport async function performAtomicIncrement<T extends string>(\n model: Model,\n field: T,\n amount = 1,\n): Promise<number> {\n model.increment(field, amount);\n return performAtomicUpdate(model, {\n $inc: {\n [field]: amount,\n },\n });\n}\n\nexport async function performAtomicDecrement<T extends string>(\n model: Model,\n field: T,\n amount = 1,\n): Promise<number> {\n model.decrement(field, amount);\n return performAtomicUpdate(model, {\n $inc: {\n [field]: -amount,\n },\n });\n}\n"],"mappings":";;;AAGA,SAAgB,qBAAqB,YAAiB,UAAqB;CAMzE,IAAI,SAAS,mBAAmB,UAAa,WAAW,mBAAmB,QACzE,WAAW,iBAAiB,SAAS;CAEvC,IAAI,SAAS,cAAc,UAAa,WAAW,cAAc,QAC/D,WAAW,YAAY,SAAS;CAElC,IAAI,SAAS,oBAAoB,UAAa,WAAW,oBAAoB,QAC3E,WAAW,kBAAkB,SAAS;CAExC,IAAI,SAAS,kBAAkB,UAAa,WAAW,kBAAkB,QACvE,WAAW,gBAAgB,SAAS;CAEtC,IAAI,SAAS,oBAAoB,UAAa,WAAW,oBAAoB,QAC3E,WAAW,kBAAkB,SAAS;CAMxC,IAAI,SAAS,oBAAoB,UAAa,WAAW,oBAAoB,QAC3E,WAAW,kBAAkB,SAAS;CAGxC,IAAI,SAAS,oBAAoB,UAAa,WAAW,oBAAoB,QAC3E,WAAW,kBAAkB,SAAS;CAMxC,IAAI,SAAS,mBAAmB,UAAa,WAAW,mBAAmB,QACzE,WAAW,iBAAiB,SAAS;CAEvC,IAAI,SAAS,oBAAoB,UAAa,WAAW,oBAAoB,QAC3E,WAAW,kBAAkB,SAAS;CAExC,IAAI,SAAS,eAAe,UAAa,WAAW,eAAe,QAEjE,IAAI,OAAO,SAAS,eAAe,YACjC,WAAW,aAAa,SAAS,WAAW,WAAW,KAAK;MAE5D,WAAW,aAAa,SAAS;CAOrC,IAAI,SAAS,eAAe,UAAa,WAAW,eAAe,QACjE,WAAW,aAAa,SAAS;AAErC;AAEA,eAAsB,oBAAoB,OAAwC;CAEhF,MAAM,IADa,eAAe,KACvB,CAAC,CAAC,eAAe;CAC5B,OAAO,MAAM;AACf;AAEA,eAAsB,oBACpB,OACA,YACiB;CACjB,OAAO,MAAM,KAAK,CAAC,CAAC,OAAO,EAAE,IAAI,MAAM,GAAI,GAAG,UAAU;AAC1D;AAEA,eAAsB,uBACpB,OACA,OACA,SAAS,GACQ;CACjB,MAAM,UAAU,OAAO,MAAM;CAC7B,OAAO,oBAAoB,OAAO,EAChC,MAAM,GACH,QAAQ,OACX,EACF,CAAC;AACH;AAEA,eAAsB,uBACpB,OACA,OACA,SAAS,GACQ;CACjB,MAAM,UAAU,OAAO,MAAM;CAC7B,OAAO,oBAAoB,OAAO,EAChC,MAAM,GACH,QAAQ,CAAC,OACZ,EACF,CAAC;AACH"}
1
+ {"version":3,"file":"meta-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/meta-methods.ts"],"sourcesContent":["import { get, set } from \"@mongez/reinforcements\";\r\nimport { DatabaseWriter } from \"../../writer/database-writer\";\r\nimport type { Model } from \"../model\";\r\n\r\n/**\r\n * Whether a subclass (or an intermediate base below `Model`) set the static\r\n * itself. The base `Model` initialises several statics (strictMode,\r\n * autoGenerateId, incrementIdBy, deletedAtColumn), so comparing to\r\n * `undefined` would never let data-source defaults apply.\r\n */\r\nfunction isExplicitStatic(ModelClass: any, key: string): boolean {\r\n let current = ModelClass;\r\n\r\n // The base Model is the class whose parent is `Function.prototype`; its own\r\n // initialisers are framework fallbacks, not explicit choices.\r\n while (current && Object.getPrototypeOf(current) !== Function.prototype) {\r\n if (Object.prototype.hasOwnProperty.call(current, key) && current[key] !== undefined) {\r\n return true;\r\n }\r\n\r\n current = Object.getPrototypeOf(current);\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function applyDefaultsToModel(ModelClass: any, defaults: any): void {\r\n // Only apply defaults if model doesn't have its own value\r\n\r\n // ============================================================================\r\n // ID Generation\r\n // ============================================================================\r\n if (defaults.autoGenerateId !== undefined && !isExplicitStatic(ModelClass, \"autoGenerateId\")) {\r\n ModelClass.autoGenerateId = defaults.autoGenerateId;\r\n }\r\n if (defaults.initialId !== undefined && !isExplicitStatic(ModelClass, \"initialId\")) {\r\n ModelClass.initialId = defaults.initialId;\r\n }\r\n if (defaults.randomInitialId !== undefined && !isExplicitStatic(ModelClass, \"randomInitialId\")) {\r\n ModelClass.randomInitialId = defaults.randomInitialId;\r\n }\r\n if (defaults.incrementIdBy !== undefined && !isExplicitStatic(ModelClass, \"incrementIdBy\")) {\r\n ModelClass.incrementIdBy = defaults.incrementIdBy;\r\n }\r\n if (defaults.randomIncrement !== undefined && !isExplicitStatic(ModelClass, \"randomIncrement\")) {\r\n ModelClass.randomIncrement = defaults.randomIncrement;\r\n }\r\n\r\n // ============================================================================\r\n // Timestamps\r\n // ============================================================================\r\n if (defaults.createdAtColumn !== undefined && !isExplicitStatic(ModelClass, \"createdAtColumn\")) {\r\n ModelClass.createdAtColumn = defaults.createdAtColumn;\r\n }\r\n\r\n if (defaults.updatedAtColumn !== undefined && !isExplicitStatic(ModelClass, \"updatedAtColumn\")) {\r\n ModelClass.updatedAtColumn = defaults.updatedAtColumn;\r\n }\r\n\r\n // ============================================================================\r\n // Deletion\r\n // ============================================================================\r\n if (defaults.deleteStrategy !== undefined && !isExplicitStatic(ModelClass, \"deleteStrategy\")) {\r\n ModelClass.deleteStrategy = defaults.deleteStrategy;\r\n }\r\n if (defaults.deletedAtColumn !== undefined && !isExplicitStatic(ModelClass, \"deletedAtColumn\")) {\r\n ModelClass.deletedAtColumn = defaults.deletedAtColumn;\r\n }\r\n if (defaults.trashTable !== undefined && !isExplicitStatic(ModelClass, \"trashTable\")) {\r\n // Handle function-based trash table\r\n if (typeof defaults.trashTable === \"function\") {\r\n ModelClass.trashTable = defaults.trashTable(ModelClass.table);\r\n } else {\r\n ModelClass.trashTable = defaults.trashTable;\r\n }\r\n }\r\n\r\n // ============================================================================\r\n // Validation\r\n // ============================================================================\r\n if (defaults.strictMode !== undefined && !isExplicitStatic(ModelClass, \"strictMode\")) {\r\n ModelClass.strictMode = defaults.strictMode;\r\n }\r\n}\r\n\r\nexport async function generateModelNextId(model: Model): Promise<number | string> {\r\n const writer = new DatabaseWriter(model);\r\n await writer.generateNextId();\r\n return model.id!;\r\n}\r\n\r\nexport async function performAtomicUpdate(\r\n model: Model,\r\n operations: Record<string, unknown>,\r\n): Promise<number> {\r\n return model.self().atomic({ [model.self().primaryKey]: model.trustedPrimaryKey }, operations);\r\n}\r\n\r\n/**\r\n * Apply an atomic delta to the local value without marking the field dirty.\r\n * The database already holds the change, so a later `save()` must not send a\r\n * stale `$set` for it. Other pending changes stay dirty.\r\n */\r\nfunction applyLocalDelta(model: Model, field: string, delta: number): void {\r\n const tracker = model.dirtyTracker;\r\n const fieldWasDirty = tracker.isDirty(field);\r\n const value = (get(model.data, field, 0) as number) + delta;\r\n\r\n set(model.data, field, value);\r\n\r\n // A removed column can't be replayed after a baseline reset, so fall back to\r\n // plain dirty tracking for that rare case.\r\n if (fieldWasDirty || tracker.getRemovedColumns().length > 0) {\r\n tracker.setAtPath(field, value);\r\n return;\r\n }\r\n\r\n const pending = tracker.getDirtyColumnsWithValues();\r\n const baseline = structuredClone(model.data) as Record<string, unknown>;\r\n\r\n for (const [column, values] of Object.entries(pending)) {\r\n set(baseline, column, values.oldValue);\r\n }\r\n\r\n tracker.reset(baseline);\r\n\r\n for (const [column, values] of Object.entries(pending)) {\r\n tracker.setAtPath(column, values.newValue);\r\n }\r\n}\r\n\r\nexport async function performAtomicIncrement<T extends string>(\r\n model: Model,\r\n field: T,\r\n amount = 1,\r\n): Promise<number> {\r\n const result = await performAtomicUpdate(model, { $inc: { [field]: amount } });\r\n applyLocalDelta(model, field, amount);\r\n return result;\r\n}\r\n\r\nexport async function performAtomicDecrement<T extends string>(\r\n model: Model,\r\n field: T,\r\n amount = 1,\r\n): Promise<number> {\r\n const result = await performAtomicUpdate(model, { $inc: { [field]: -amount } });\r\n applyLocalDelta(model, field, -amount);\r\n return result;\r\n}\r\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { dataSourceRegistry } from "../../data-source/data-source-registry.mjs";
2
2
  import { sanitizeFilter } from "../../utils/sanitize-filter.mjs";
3
+ import { emitStaticEvent } from "./static-event-methods.mjs";
3
4
  import { warnUndeclaredSensitiveFields } from "./serialization-methods.mjs";
4
5
 
5
6
  //#region ../cascade/src/model/methods/query-methods.ts
@@ -10,15 +11,29 @@ function buildQuery(ModelClass, BaseModel) {
10
11
  queryBuilder.disabledGlobalScopes = /* @__PURE__ */ new Set();
11
12
  queryBuilder.relationDefinitions = ModelClass.relations;
12
13
  queryBuilder.modelClass = ModelClass;
13
- ModelClass.events().emitFetching(queryBuilder, {
14
- table: ModelClass.table,
15
- modelClass: ModelClass
14
+ let fetchStartedAt = Date.now();
15
+ queryBuilder.onFetching(async () => {
16
+ fetchStartedAt = Date.now();
17
+ await emitStaticEvent(ModelClass, "fetching", queryBuilder, {
18
+ table: ModelClass.table,
19
+ modelClass: ModelClass
20
+ });
21
+ });
22
+ queryBuilder.onHydrating(async (records, context) => {
23
+ await emitStaticEvent(ModelClass, "hydrating", records, {
24
+ query: queryBuilder,
25
+ hydrateCallback: context?.hydrateCallback
26
+ });
16
27
  });
17
28
  queryBuilder.hydrate((data) => {
18
29
  return ModelClass.hydrate(data);
19
30
  });
20
- queryBuilder.onFetched(async (models) => {
21
- await ModelClass.events().emit("fetched", models, {});
31
+ queryBuilder.onFetched(async (models, context) => {
32
+ await emitStaticEvent(ModelClass, "fetched", models, {
33
+ query: queryBuilder,
34
+ rawRecords: context?.rawRecords ?? [],
35
+ duration: context?.duration ?? Date.now() - fetchStartedAt
36
+ });
22
37
  });
23
38
  return queryBuilder;
24
39
  }
@@ -81,35 +96,91 @@ function toDriverOptions(options) {
81
96
  return rest;
82
97
  }
83
98
  /**
99
+ * Pin a driver-level write to the rows the model's global scopes (tenant, soft
100
+ * delete) allow. The driver write APIs take a raw filter and never see scopes,
101
+ * so the target ids are resolved through the scoped query builder and added to
102
+ * the filter. `null` means no visible row matches: the write must be a no-op.
103
+ */
104
+ async function scopeWriteFilter(ModelClass, filter, single) {
105
+ const primaryKey = ModelClass.primaryKey;
106
+ const ids = await ModelClass.query().where(filter).pluck(primaryKey);
107
+ if (ids.length === 0) return null;
108
+ return {
109
+ ...filter,
110
+ [primaryKey]: single ? ids[0] : { $in: ids }
111
+ };
112
+ }
113
+ /**
114
+ * Scoped filter for a write that may insert (`upsert`). With no visible match
115
+ * the driver would insert or, worse, update a row a scope hides (another
116
+ * tenant's, a soft-deleted one), so a hidden match is refused.
117
+ */
118
+ async function scopeUpsertFilter(ModelClass, filter) {
119
+ const scoped = await scopeWriteFilter(ModelClass, filter, true);
120
+ if (scoped) return scoped;
121
+ if (await ModelClass.query().withoutGlobalScopes().where(filter).exists()) throw new Error(`${ModelClass.name}: upsert matches a row hidden by a global scope (tenant or soft delete); refusing to touch it.`);
122
+ return filter;
123
+ }
124
+ /**
84
125
  * Run an atomic update and return the number of documents modified plus the
85
126
  * number inserted by an `upsert`.
86
127
  */
87
128
  async function performAtomic(ModelClass, filter, operations, options) {
88
- const result = await ModelClass.getDriver().atomic(ModelClass.table, resolveFilter(filter, options), operations, toDriverOptions(options));
129
+ const resolved = resolveFilter(filter, options);
130
+ const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved) : await scopeWriteFilter(ModelClass, resolved, false);
131
+ if (!scoped) return 0;
132
+ const result = await ModelClass.getDriver().atomic(ModelClass.table, scoped, operations, toDriverOptions(options));
89
133
  return result.modifiedCount + (result.upsertedCount ?? 0);
90
134
  }
135
+ /**
136
+ * Update a row by id through the model writer: the row is loaded (scopes
137
+ * apply), merged, and saved, so the schema, strictMode, casting, `updatedAt`
138
+ * and events all apply. Returns 1 when a row was saved, 0 when none is visible.
139
+ */
91
140
  async function updateById(ModelClass, id, data) {
92
- return (await ModelClass.getDriver().update(ModelClass.table, { [ModelClass.primaryKey]: id }, { $set: data })).modifiedCount;
141
+ const model = await ModelClass.query().where(ModelClass.primaryKey, id).first();
142
+ if (!model) return 0;
143
+ model.merge(data);
144
+ await model.save();
145
+ return 1;
93
146
  }
94
147
  async function findAndUpdateRecords(ModelClass, filter, update, options) {
95
- await performAtomic(ModelClass, filter, update, {
148
+ const primaryKey = ModelClass.primaryKey;
149
+ const safeFilter = sanitizeFilter(filter);
150
+ const ids = await ModelClass.query().where(safeFilter).pluck(primaryKey);
151
+ if (ids.length === 0) {
152
+ if (!options?.upsert) return [];
153
+ await performAtomic(ModelClass, safeFilter, update, {
154
+ ...options,
155
+ trustedFilter: true
156
+ });
157
+ return await ModelClass.query().where(safeFilter).get();
158
+ }
159
+ await performAtomic(ModelClass, { [primaryKey]: { $in: ids } }, update, {
96
160
  ...options,
97
- trustedFilter: false
161
+ trustedFilter: true
98
162
  });
99
- return await ModelClass.query().where(filter).get();
163
+ return await ModelClass.query().whereIn(primaryKey, ids).get();
100
164
  }
101
165
  async function findOneAndUpdateRecord(ModelClass, filter, update, options) {
102
- const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, resolveFilter(filter, options), update, toDriverOptions(options));
166
+ const resolved = resolveFilter(filter, options);
167
+ const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved) : await scopeWriteFilter(ModelClass, resolved, true);
168
+ if (!scoped) return null;
169
+ const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, scoped, update, toDriverOptions(options));
103
170
  if (!result) return null;
104
- return new ModelClass(result);
171
+ return ModelClass.hydrate(result);
105
172
  }
106
173
  async function findAndReplaceRecord(ModelClass, filter, document) {
107
- const result = await ModelClass.getDriver().replace(ModelClass.table, sanitizeFilter(filter), document);
174
+ const scoped = await scopeWriteFilter(ModelClass, sanitizeFilter(filter), true);
175
+ if (!scoped) return null;
176
+ const result = await ModelClass.getDriver().replace(ModelClass.table, scoped, document);
108
177
  if (!result) return null;
109
- return new ModelClass(result);
178
+ return ModelClass.hydrate(result);
110
179
  }
111
180
  async function findOneAndDeleteRecord(ModelClass, filter, options) {
112
- const result = await ModelClass.getDriver().findOneAndDelete(ModelClass.table, sanitizeFilter(filter), options);
181
+ const scoped = await scopeWriteFilter(ModelClass, sanitizeFilter(filter), true);
182
+ if (!scoped) return null;
183
+ const result = await ModelClass.getDriver().findOneAndDelete(ModelClass.table, scoped, options);
113
184
  if (!result) return null;
114
185
  const model = ModelClass.hydrate(result);
115
186
  model.dirtyTracker.reset();
@@ -1 +1 @@
1
- {"version":3,"file":"query-methods.mjs","names":["ctor"],"sources":["../../../../../../../../cascade/src/model/methods/query-methods.ts"],"sourcesContent":["import type {\r\n AtomicUpdate,\r\n AtomicUpdateOptions,\r\n FindOneAndUpdateOptions,\r\n PaginationOptions,\r\n PaginationResult,\r\n QueryBuilderContract,\r\n} from \"../../contracts\";\r\nimport type { DataSource } from \"../../data-source/data-source\";\r\nimport { dataSourceRegistry } from \"../../data-source/data-source-registry\";\r\nimport { sanitizeFilter } from \"../../utils/sanitize-filter\";\r\nimport type { ChildModel, GlobalScopeDefinition, Model } from \"../model\";\r\nimport { warnUndeclaredSensitiveFields } from \"./serialization-methods\";\r\n\r\nexport function buildQuery<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n BaseModel: typeof Model,\r\n): QueryBuilderContract<TModel> {\r\n const queryBuilder = ModelClass.newQueryBuilder<TModel>();\r\n const qb = queryBuilder;\r\n\r\n // Collect global scopes from base Model and child model\r\n const allGlobalScopes = new Map<string, GlobalScopeDefinition>([\r\n ...BaseModel.globalScopes,\r\n ...ModelClass.globalScopes,\r\n ]);\r\n\r\n queryBuilder.pendingGlobalScopes = allGlobalScopes;\r\n queryBuilder.availableLocalScopes = ModelClass.localScopes;\r\n queryBuilder.disabledGlobalScopes = new Set();\r\n queryBuilder.relationDefinitions = ModelClass.relations;\r\n queryBuilder.modelClass = ModelClass;\r\n\r\n ModelClass.events().emitFetching(queryBuilder, {\r\n table: ModelClass.table,\r\n modelClass: ModelClass,\r\n });\r\n\r\n queryBuilder.hydrate((data: any) => {\r\n return ModelClass.hydrate(data);\r\n });\r\n\r\n // Eager-loading runs inside the driver's `get()` so it works for any\r\n // builder construction path (Model.query, Model.newQueryBuilder, custom\r\n // subclassed builders). This hook only handles the model-level `fetched`\r\n // event emission.\r\n queryBuilder.onFetched(async (models: any[]) => {\r\n await ModelClass.events().emit(\"fetched\", models as any, {});\r\n });\r\n\r\n return queryBuilder;\r\n}\r\n\r\nexport function buildNewQueryBuilder<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n): QueryBuilderContract<TModel> {\r\n const dataSource = ModelClass.getDataSource();\r\n\r\n if (ModelClass.builder) {\r\n const BuilderClass = ModelClass.builder;\r\n return new BuilderClass(ModelClass.table, dataSource) as QueryBuilderContract<TModel>;\r\n }\r\n\r\n const queryBuilder = dataSource.driver.queryBuilder<TModel>(ModelClass.table);\r\n return queryBuilder;\r\n}\r\n\r\nexport async function findFirst<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter?: Record<string, unknown>,\r\n): Promise<TModel | null> {\r\n const query = ModelClass.query();\r\n if (filter) {\r\n query.where(filter);\r\n }\r\n return query.first();\r\n}\r\n\r\nexport async function findLast<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter?: Record<string, unknown>,\r\n): Promise<TModel | null> {\r\n const query = ModelClass.query();\r\n if (filter) {\r\n query.where(filter);\r\n }\r\n\r\n return query.last();\r\n}\r\n\r\nexport async function findAll<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter?: Record<string, unknown>,\r\n): Promise<TModel[]> {\r\n const query = ModelClass.query();\r\n if (filter) {\r\n query.where(filter);\r\n }\r\n return query.get();\r\n}\r\n\r\nexport function countRecords<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter?: Record<string, unknown>,\r\n): Promise<number> {\r\n const query = ModelClass.query();\r\n if (filter) {\r\n query.where(filter);\r\n }\r\n return query.count();\r\n}\r\n\r\nexport async function findById<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n id: string | number,\r\n): Promise<TModel | null> {\r\n const query = ModelClass.query();\r\n return query.where(ModelClass.primaryKey, id).first();\r\n}\r\n\r\nexport async function paginateRecords<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n options: PaginationOptions & { filter?: Record<string, unknown> } = {},\r\n): Promise<PaginationResult<TModel>> {\r\n const query = ModelClass.query();\r\n if (options.filter) {\r\n query.where(options.filter);\r\n }\r\n\r\n return query.paginate({\r\n limit: options.limit,\r\n page: options.page,\r\n });\r\n}\r\n\r\nexport async function findLatest<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter?: Record<string, unknown>,\r\n): Promise<TModel[]> {\r\n const query = ModelClass.query();\r\n if (filter) {\r\n query.where(filter);\r\n }\r\n return (await query.latest()) as unknown as TModel[];\r\n}\r\n\r\nexport function increaseField<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n field: string,\r\n amount: number,\r\n): Promise<number> {\r\n const query = ModelClass.query().where(filter);\r\n return query.increment(field, amount);\r\n}\r\n\r\nexport function decreaseField<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n field: string,\r\n amount: number,\r\n): Promise<number> {\r\n const query = ModelClass.query().where(filter);\r\n return query.decrement(field, amount);\r\n}\r\n\r\n// The atomic/find-and-modify statics below forward their filter straight to\r\n// the driver — they never pass through where(), so they must run the same\r\n// operator-injection check themselves. Only the FILTER is sanitized; update\r\n// operators ($set/$inc/…) are the point of these APIs and stay untouched.\r\n//\r\n// `trustedFilter` is the explicit, code-authored opt-out: it lets a conditional\r\n// filter such as `{ used: { $lt: 10 } }` through. It is stripped before the\r\n// options reach the driver.\r\nfunction resolveFilter(\r\n filter: Record<string, unknown>,\r\n options: AtomicUpdateOptions | undefined,\r\n): Record<string, unknown> {\r\n return options?.trustedFilter ? filter : sanitizeFilter(filter);\r\n}\r\n\r\nfunction toDriverOptions<TOptions extends AtomicUpdateOptions>(\r\n options: TOptions | undefined,\r\n): Omit<TOptions, \"trustedFilter\"> | undefined {\r\n if (!options) {\r\n return undefined;\r\n }\r\n\r\n const { trustedFilter: _trustedFilter, ...rest } = options;\r\n\r\n return rest;\r\n}\r\n\r\n/**\r\n * Run an atomic update and return the number of documents modified plus the\r\n * number inserted by an `upsert`.\r\n */\r\nexport async function performAtomic<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n operations: AtomicUpdate,\r\n options?: AtomicUpdateOptions,\r\n): Promise<number> {\r\n const result = await ModelClass.getDriver().atomic(\r\n ModelClass.table,\r\n resolveFilter(filter, options),\r\n operations,\r\n toDriverOptions(options),\r\n );\r\n\r\n return result.modifiedCount + (result.upsertedCount ?? 0);\r\n}\r\n\r\nexport async function updateById<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n id: string | number,\r\n data: Record<string, unknown>,\r\n): Promise<number> {\r\n const result = await ModelClass.getDriver().update(ModelClass.table, { [ModelClass.primaryKey]: id }, { $set: data });\r\n return result.modifiedCount;\r\n}\r\n\r\nexport async function findAndUpdateRecords<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n update: AtomicUpdate,\r\n options?: Omit<AtomicUpdateOptions, \"trustedFilter\">,\r\n): Promise<TModel[]> {\r\n await performAtomic(ModelClass, filter, update, { ...options, trustedFilter: false });\r\n return await ModelClass.query().where(filter).get();\r\n}\r\n\r\nexport async function findOneAndUpdateRecord<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n update: AtomicUpdate,\r\n options?: FindOneAndUpdateOptions,\r\n): Promise<TModel | null> {\r\n const result = await ModelClass.getDriver().findOneAndUpdate(\r\n ModelClass.table,\r\n resolveFilter(filter, options),\r\n update,\r\n toDriverOptions(options),\r\n );\r\n if (!result) return null;\r\n const ctor = ModelClass as any;\r\n return new ctor(result);\r\n}\r\n\r\nexport async function findAndReplaceRecord<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n document: Record<string, unknown>,\r\n): Promise<TModel | null> {\r\n const result = await ModelClass.getDriver().replace(\r\n ModelClass.table,\r\n sanitizeFilter(filter),\r\n document,\r\n );\r\n if (!result) return null;\r\n const ctor = ModelClass as any;\r\n return new ctor(result);\r\n}\r\n\r\nexport async function findOneAndDeleteRecord<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n filter: Record<string, unknown>,\r\n options?: Record<string, unknown>,\r\n): Promise<TModel | null> {\r\n const driver = ModelClass.getDriver();\r\n const result = await driver.findOneAndDelete(ModelClass.table, sanitizeFilter(filter), options);\r\n\r\n if (!result) {\r\n return null;\r\n }\r\n\r\n const model = ModelClass.hydrate(result as Record<string, unknown>);\r\n model.dirtyTracker.reset();\r\n return model;\r\n}\r\n\r\nexport function resolveDataSource<TModel extends Model>(\r\n ModelClass: ChildModel<TModel>,\r\n): DataSource {\r\n const ref = ModelClass.dataSource;\r\n let dataSource: DataSource;\r\n\r\n if (typeof ref === \"string\") {\r\n dataSource = dataSourceRegistry.get(ref);\r\n } else if (ref) {\r\n dataSource = ref;\r\n } else {\r\n dataSource = dataSourceRegistry.get();\r\n }\r\n\r\n if (!ModelClass.hasOwnProperty(\"_defaultsApplied\")) {\r\n const driverDefaults = dataSource.driver.modelDefaults || {};\r\n const dataSourceDefaults = dataSource.modelDefaults || {};\r\n\r\n const mergedDefaults = {\r\n ...driverDefaults,\r\n ...dataSourceDefaults,\r\n };\r\n\r\n if (Object.keys(mergedDefaults).length > 0) {\r\n (ModelClass as any).applyModelDefaults(mergedDefaults);\r\n }\r\n\r\n (ModelClass as any)._defaultsApplied = true;\r\n }\r\n\r\n // First data-source resolution is the earliest per-class hook every model\r\n // passes through — warn here (once) if the schema carries credential-shaped\r\n // fields that toJSON() would expose.\r\n warnUndeclaredSensitiveFields(ModelClass);\r\n\r\n return dataSource;\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"query-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/query-methods.ts"],"sourcesContent":["import type {\n AtomicUpdate,\n AtomicUpdateOptions,\n FindOneAndUpdateOptions,\n PaginationOptions,\n PaginationResult,\n QueryBuilderContract,\n} from \"../../contracts\";\nimport type { DataSource } from \"../../data-source/data-source\";\nimport { dataSourceRegistry } from \"../../data-source/data-source-registry\";\nimport { sanitizeFilter } from \"../../utils/sanitize-filter\";\nimport type { ChildModel, GlobalScopeDefinition, Model } from \"../model\";\nimport { emitStaticEvent } from \"./static-event-methods\";\nimport { warnUndeclaredSensitiveFields } from \"./serialization-methods\";\n\nexport function buildQuery<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n BaseModel: typeof Model,\n): QueryBuilderContract<TModel> {\n const queryBuilder = ModelClass.newQueryBuilder<TModel>();\n const qb = queryBuilder;\n\n // Collect global scopes from base Model and child model\n const allGlobalScopes = new Map<string, GlobalScopeDefinition>([\n ...BaseModel.globalScopes,\n ...ModelClass.globalScopes,\n ]);\n\n queryBuilder.pendingGlobalScopes = allGlobalScopes;\n queryBuilder.availableLocalScopes = ModelClass.localScopes;\n queryBuilder.disabledGlobalScopes = new Set();\n queryBuilder.relationDefinitions = ModelClass.relations;\n queryBuilder.modelClass = ModelClass;\n\n let fetchStartedAt = Date.now();\n\n // Emitted from the builder's execute path (awaited), not at construction, so\n // listener changes apply before the query runs and a throwing listener fails\n // the query instead of becoming an unhandled rejection.\n queryBuilder.onFetching(async () => {\n fetchStartedAt = Date.now();\n\n await emitStaticEvent(ModelClass, \"fetching\", queryBuilder, {\n table: ModelClass.table,\n modelClass: ModelClass,\n });\n });\n\n queryBuilder.onHydrating(async (records: any[], context: any) => {\n await emitStaticEvent(ModelClass, \"hydrating\", records as any, {\n query: queryBuilder,\n hydrateCallback: context?.hydrateCallback,\n });\n });\n\n queryBuilder.hydrate((data: any) => {\n return ModelClass.hydrate(data);\n });\n\n // Eager-loading runs inside the driver's `get()` so it works for any\n // builder construction path (Model.query, Model.newQueryBuilder, custom\n // subclassed builders). This hook only handles the model-level `fetched`\n // event emission.\n queryBuilder.onFetched(async (models: any[], context: any) => {\n await emitStaticEvent(ModelClass, \"fetched\", models as any, {\n query: queryBuilder,\n rawRecords: context?.rawRecords ?? [],\n duration: context?.duration ?? Date.now() - fetchStartedAt,\n });\n });\n\n return queryBuilder;\n}\n\nexport function buildNewQueryBuilder<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n): QueryBuilderContract<TModel> {\n const dataSource = ModelClass.getDataSource();\n\n if (ModelClass.builder) {\n const BuilderClass = ModelClass.builder;\n return new BuilderClass(ModelClass.table, dataSource) as QueryBuilderContract<TModel>;\n }\n\n const queryBuilder = dataSource.driver.queryBuilder<TModel>(ModelClass.table);\n return queryBuilder;\n}\n\nexport async function findFirst<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter?: Record<string, unknown>,\n): Promise<TModel | null> {\n const query = ModelClass.query();\n if (filter) {\n query.where(filter);\n }\n return query.first();\n}\n\nexport async function findLast<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter?: Record<string, unknown>,\n): Promise<TModel | null> {\n const query = ModelClass.query();\n if (filter) {\n query.where(filter);\n }\n\n return query.last();\n}\n\nexport async function findAll<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter?: Record<string, unknown>,\n): Promise<TModel[]> {\n const query = ModelClass.query();\n if (filter) {\n query.where(filter);\n }\n return query.get();\n}\n\nexport function countRecords<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter?: Record<string, unknown>,\n): Promise<number> {\n const query = ModelClass.query();\n if (filter) {\n query.where(filter);\n }\n return query.count();\n}\n\nexport async function findById<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n id: string | number,\n): Promise<TModel | null> {\n const query = ModelClass.query();\n return query.where(ModelClass.primaryKey, id).first();\n}\n\nexport async function paginateRecords<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n options: PaginationOptions & { filter?: Record<string, unknown> } = {},\n): Promise<PaginationResult<TModel>> {\n const query = ModelClass.query();\n if (options.filter) {\n query.where(options.filter);\n }\n\n return query.paginate({\n limit: options.limit,\n page: options.page,\n });\n}\n\nexport async function findLatest<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter?: Record<string, unknown>,\n): Promise<TModel[]> {\n const query = ModelClass.query();\n if (filter) {\n query.where(filter);\n }\n return (await query.latest()) as unknown as TModel[];\n}\n\nexport function increaseField<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n field: string,\n amount: number,\n): Promise<number> {\n const query = ModelClass.query().where(filter);\n return query.increment(field, amount);\n}\n\nexport function decreaseField<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n field: string,\n amount: number,\n): Promise<number> {\n const query = ModelClass.query().where(filter);\n return query.decrement(field, amount);\n}\n\n// The atomic/find-and-modify statics below forward their filter straight to\n// the driver — they never pass through where(), so they must run the same\n// operator-injection check themselves. Only the FILTER is sanitized; update\n// operators ($set/$inc/…) are the point of these APIs and stay untouched.\n//\n// `trustedFilter` is the explicit, code-authored opt-out: it lets a conditional\n// filter such as `{ used: { $lt: 10 } }` through. It is stripped before the\n// options reach the driver.\nfunction resolveFilter(\n filter: Record<string, unknown>,\n options: AtomicUpdateOptions | undefined,\n): Record<string, unknown> {\n return options?.trustedFilter ? filter : sanitizeFilter(filter);\n}\n\nfunction toDriverOptions<TOptions extends AtomicUpdateOptions>(\n options: TOptions | undefined,\n): Omit<TOptions, \"trustedFilter\"> | undefined {\n if (!options) {\n return undefined;\n }\n\n const { trustedFilter: _trustedFilter, ...rest } = options;\n\n return rest;\n}\n\n/**\n * Pin a driver-level write to the rows the model's global scopes (tenant, soft\n * delete) allow. The driver write APIs take a raw filter and never see scopes,\n * so the target ids are resolved through the scoped query builder and added to\n * the filter. `null` means no visible row matches: the write must be a no-op.\n */\nexport async function scopeWriteFilter(\n ModelClass: ChildModel<any>,\n filter: Record<string, unknown>,\n single: boolean,\n): Promise<Record<string, unknown> | null> {\n const primaryKey = ModelClass.primaryKey;\n const ids = await ModelClass.query().where(filter).pluck(primaryKey);\n\n if (ids.length === 0) {\n return null;\n }\n\n return { ...filter, [primaryKey]: single ? ids[0] : { $in: ids } };\n}\n\n/**\n * Scoped filter for a write that may insert (`upsert`). With no visible match\n * the driver would insert or, worse, update a row a scope hides (another\n * tenant's, a soft-deleted one), so a hidden match is refused.\n */\nexport async function scopeUpsertFilter(\n ModelClass: ChildModel<any>,\n filter: Record<string, unknown>,\n): Promise<Record<string, unknown>> {\n const scoped = await scopeWriteFilter(ModelClass, filter, true);\n\n if (scoped) {\n return scoped;\n }\n\n const hidden = await ModelClass.query().withoutGlobalScopes().where(filter).exists();\n\n if (hidden) {\n throw new Error(\n `${ModelClass.name}: upsert matches a row hidden by a global scope (tenant or soft delete); refusing to touch it.`,\n );\n }\n\n return filter;\n}\n\n/**\n * Run an atomic update and return the number of documents modified plus the\n * number inserted by an `upsert`.\n */\nexport async function performAtomic<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n operations: AtomicUpdate,\n options?: AtomicUpdateOptions,\n): Promise<number> {\n const resolved = resolveFilter(filter, options);\n const scoped = options?.upsert\n ? await scopeUpsertFilter(ModelClass, resolved)\n : await scopeWriteFilter(ModelClass, resolved, false);\n\n if (!scoped) {\n return 0;\n }\n\n const result = await ModelClass.getDriver().atomic(\n ModelClass.table,\n scoped,\n operations,\n toDriverOptions(options),\n );\n\n return result.modifiedCount + (result.upsertedCount ?? 0);\n}\n\n/**\n * Update a row by id through the model writer: the row is loaded (scopes\n * apply), merged, and saved, so the schema, strictMode, casting, `updatedAt`\n * and events all apply. Returns 1 when a row was saved, 0 when none is visible.\n */\nexport async function updateById<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n id: string | number,\n data: Record<string, unknown>,\n): Promise<number> {\n const model = await ModelClass.query().where(ModelClass.primaryKey, id).first();\n\n if (!model) {\n return 0;\n }\n\n model.merge(data);\n await model.save();\n\n return 1;\n}\n\nexport async function findAndUpdateRecords<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n update: AtomicUpdate,\n options?: Omit<AtomicUpdateOptions, \"trustedFilter\">,\n): Promise<TModel[]> {\n const primaryKey = ModelClass.primaryKey;\n const safeFilter = sanitizeFilter(filter);\n\n // Capture the matching ids first: the update usually changes the very field\n // the filter matches on, so re-querying with the filter would find nothing.\n const ids = await ModelClass.query().where(safeFilter).pluck(primaryKey);\n\n if (ids.length === 0) {\n if (!options?.upsert) {\n return [];\n }\n\n await performAtomic(ModelClass, safeFilter, update, { ...options, trustedFilter: true });\n return await ModelClass.query().where(safeFilter).get();\n }\n\n await performAtomic(\n ModelClass,\n { [primaryKey]: { $in: ids } },\n update,\n { ...options, trustedFilter: true },\n );\n\n return await ModelClass.query().whereIn(primaryKey, ids).get();\n}\n\nexport async function findOneAndUpdateRecord<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n update: AtomicUpdate,\n options?: FindOneAndUpdateOptions,\n): Promise<TModel | null> {\n const resolved = resolveFilter(filter, options);\n const scoped = options?.upsert\n ? await scopeUpsertFilter(ModelClass, resolved)\n : await scopeWriteFilter(ModelClass, resolved, true);\n\n if (!scoped) return null;\n\n const result = await ModelClass.getDriver().findOneAndUpdate(\n ModelClass.table,\n scoped,\n update,\n toDriverOptions(options),\n );\n if (!result) return null;\n return ModelClass.hydrate(result as Record<string, unknown>) as TModel;\n}\n\nexport async function findAndReplaceRecord<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n document: Record<string, unknown>,\n): Promise<TModel | null> {\n const scoped = await scopeWriteFilter(ModelClass, sanitizeFilter(filter), true);\n\n if (!scoped) return null;\n\n const result = await ModelClass.getDriver().replace(ModelClass.table, scoped, document);\n if (!result) return null;\n return ModelClass.hydrate(result as Record<string, unknown>) as TModel;\n}\n\nexport async function findOneAndDeleteRecord<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n filter: Record<string, unknown>,\n options?: Record<string, unknown>,\n): Promise<TModel | null> {\n const scoped = await scopeWriteFilter(ModelClass, sanitizeFilter(filter), true);\n\n if (!scoped) {\n return null;\n }\n\n const result = await ModelClass.getDriver().findOneAndDelete(ModelClass.table, scoped, options);\n\n if (!result) {\n return null;\n }\n\n const model = ModelClass.hydrate(result as Record<string, unknown>);\n model.dirtyTracker.reset();\n return model;\n}\n\nexport function resolveDataSource<TModel extends Model>(\n ModelClass: ChildModel<TModel>,\n): DataSource {\n const ref = ModelClass.dataSource;\n let dataSource: DataSource;\n\n if (typeof ref === \"string\") {\n dataSource = dataSourceRegistry.get(ref);\n } else if (ref) {\n dataSource = ref;\n } else {\n dataSource = dataSourceRegistry.get();\n }\n\n if (!ModelClass.hasOwnProperty(\"_defaultsApplied\")) {\n const driverDefaults = dataSource.driver.modelDefaults || {};\n const dataSourceDefaults = dataSource.modelDefaults || {};\n\n const mergedDefaults = {\n ...driverDefaults,\n ...dataSourceDefaults,\n };\n\n if (Object.keys(mergedDefaults).length > 0) {\n (ModelClass as any).applyModelDefaults(mergedDefaults);\n }\n\n (ModelClass as any)._defaultsApplied = true;\n }\n\n // First data-source resolution is the earliest per-class hook every model\n // passes through — warn here (once) if the schema carries credential-shaped\n // fields that toJSON() would expose.\n warnUndeclaredSensitiveFields(ModelClass);\n\n return dataSource;\n}\n"],"mappings":"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"}
@@ -15,6 +15,21 @@ function getModelEvents(ModelClass) {
15
15
  }
16
16
  return events;
17
17
  }
18
+ /**
19
+ * Emit a static event to the class's listeners and then to every ancestor
20
+ * class's listeners up to `Model`, so a listener registered on a shared base
21
+ * (`class AppModel extends Model`) fires for its subclasses too. Ancestors are
22
+ * only read, never given an emitter they don't already have.
23
+ */
24
+ async function emitStaticEvent(ModelClass, event, model, context) {
25
+ await ModelClass.events().emit(event, model, context);
26
+ let ancestor = Object.getPrototypeOf(ModelClass);
27
+ while (ancestor && ancestor !== Function.prototype) {
28
+ const events = modelEventsRegistry.get(ancestor);
29
+ if (events) await events.emit(event, model, context);
30
+ ancestor = Object.getPrototypeOf(ancestor);
31
+ }
32
+ }
18
33
  function cleanupModelEvents(ModelClass) {
19
34
  modelEventsRegistry.delete(ModelClass);
20
35
  removeModelFromRegistery(ModelClass.name);
@@ -33,5 +48,5 @@ function getGlobalEvents() {
33
48
  }
34
49
 
35
50
  //#endregion
36
- export { cleanupModelEvents, getGlobalEvents, getModelEvents, offStaticEvent, onStaticEvent, onceStaticEvent };
51
+ export { cleanupModelEvents, emitStaticEvent, getGlobalEvents, getModelEvents, offStaticEvent, onStaticEvent, onceStaticEvent };
37
52
  //# sourceMappingURL=static-event-methods.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"static-event-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/static-event-methods.ts"],"sourcesContent":["import {\n ModelEvents,\n globalModelEvents,\n type ModelEventListener,\n type ModelEventName,\n} from \"../../events/model-events\";\nimport { removeModelFromRegistery } from \"../register-model\";\nimport type { ChildModel, Model } from \"../model\";\n\n/**\n * Isolated event emitter registry — one ModelEvents instance per model constructor.\n * Encapsulated here so no other module needs to know it exists.\n */\nconst modelEventsRegistry = new WeakMap<any, any>();\n\nexport function getModelEvents<TModel extends Model>(\n ModelClass: any,\n): ModelEvents<TModel> {\n let events = modelEventsRegistry.get(ModelClass);\n if (!events) {\n events = new ModelEvents<TModel>();\n modelEventsRegistry.set(ModelClass, events);\n }\n return events as ModelEvents<TModel>;\n}\n\nexport function cleanupModelEvents(ModelClass: any): void {\n modelEventsRegistry.delete(ModelClass);\n removeModelFromRegistery(ModelClass.name);\n}\n\nexport function onStaticEvent<TModel extends Model = Model, TContext = unknown>(\n ModelClass: ChildModel<TModel>,\n event: ModelEventName,\n listener: ModelEventListener<TModel, TContext>,\n): () => void {\n return ModelClass.events<TModel>().on(event, listener);\n}\n\nexport function onceStaticEvent<TModel extends Model = Model, TContext = unknown>(\n ModelClass: ChildModel<TModel>,\n event: ModelEventName,\n listener: ModelEventListener<TModel, TContext>,\n): () => void {\n return ModelClass.events<TModel>().once(event, listener);\n}\n\nexport function offStaticEvent<TModel extends Model = Model, TContext = unknown>(\n ModelClass: ChildModel<TModel>,\n event: ModelEventName,\n listener: ModelEventListener<TModel, TContext>,\n): void {\n ModelClass.events<TModel>().off(event, listener);\n}\n\nexport function getGlobalEvents(): ModelEvents<Model> {\n return globalModelEvents;\n}\n"],"mappings":";;;;;;;;AAaA,MAAM,sCAAsB,IAAI,QAAkB;AAElD,SAAgB,eACd,YACqB;CACrB,IAAI,SAAS,oBAAoB,IAAI,UAAU;CAC/C,IAAI,CAAC,QAAQ;EACX,SAAS,IAAI,YAAoB;EACjC,oBAAoB,IAAI,YAAY,MAAM;CAC5C;CACA,OAAO;AACT;AAEA,SAAgB,mBAAmB,YAAuB;CACxD,oBAAoB,OAAO,UAAU;CACrC,yBAAyB,WAAW,IAAI;AAC1C;AAEA,SAAgB,cACd,YACA,OACA,UACY;CACZ,OAAO,WAAW,OAAe,CAAC,CAAC,GAAG,OAAO,QAAQ;AACvD;AAEA,SAAgB,gBACd,YACA,OACA,UACY;CACZ,OAAO,WAAW,OAAe,CAAC,CAAC,KAAK,OAAO,QAAQ;AACzD;AAEA,SAAgB,eACd,YACA,OACA,UACM;CACN,WAAW,OAAe,CAAC,CAAC,IAAI,OAAO,QAAQ;AACjD;AAEA,SAAgB,kBAAsC;CACpD,OAAO;AACT"}
1
+ {"version":3,"file":"static-event-methods.mjs","names":[],"sources":["../../../../../../../../cascade/src/model/methods/static-event-methods.ts"],"sourcesContent":["import {\n ModelEvents,\n globalModelEvents,\n type ModelEventListener,\n type ModelEventName,\n} from \"../../events/model-events\";\nimport { removeModelFromRegistery } from \"../register-model\";\nimport type { ChildModel, Model } from \"../model\";\n\n/**\n * Isolated event emitter registry — one ModelEvents instance per model constructor.\n * Encapsulated here so no other module needs to know it exists.\n */\nconst modelEventsRegistry = new WeakMap<any, any>();\n\nexport function getModelEvents<TModel extends Model>(\n ModelClass: any,\n): ModelEvents<TModel> {\n let events = modelEventsRegistry.get(ModelClass);\n if (!events) {\n events = new ModelEvents<TModel>();\n modelEventsRegistry.set(ModelClass, events);\n }\n return events as ModelEvents<TModel>;\n}\n\n/**\n * Emit a static event to the class's listeners and then to every ancestor\n * class's listeners up to `Model`, so a listener registered on a shared base\n * (`class AppModel extends Model`) fires for its subclasses too. Ancestors are\n * only read, never given an emitter they don't already have.\n */\nexport async function emitStaticEvent<TContext = unknown>(\n ModelClass: any,\n event: ModelEventName,\n model: any,\n context: TContext,\n): Promise<void> {\n await ModelClass.events().emit(event, model, context);\n\n let ancestor = Object.getPrototypeOf(ModelClass);\n\n while (ancestor && ancestor !== Function.prototype) {\n const events = modelEventsRegistry.get(ancestor);\n\n if (events) {\n await events.emit(event, model, context);\n }\n\n ancestor = Object.getPrototypeOf(ancestor);\n }\n}\n\nexport function cleanupModelEvents(ModelClass: any): void {\n modelEventsRegistry.delete(ModelClass);\n removeModelFromRegistery(ModelClass.name);\n}\n\nexport function onStaticEvent<TModel extends Model = Model, TContext = unknown>(\n ModelClass: ChildModel<TModel>,\n event: ModelEventName,\n listener: ModelEventListener<TModel, TContext>,\n): () => void {\n return ModelClass.events<TModel>().on(event, listener);\n}\n\nexport function onceStaticEvent<TModel extends Model = Model, TContext = unknown>(\n ModelClass: ChildModel<TModel>,\n event: ModelEventName,\n listener: ModelEventListener<TModel, TContext>,\n): () => void {\n return ModelClass.events<TModel>().once(event, listener);\n}\n\nexport function offStaticEvent<TModel extends Model = Model, TContext = unknown>(\n ModelClass: ChildModel<TModel>,\n event: ModelEventName,\n listener: ModelEventListener<TModel, TContext>,\n): void {\n ModelClass.events<TModel>().off(event, listener);\n}\n\nexport function getGlobalEvents(): ModelEvents<Model> {\n return globalModelEvents;\n}\n"],"mappings":";;;;;;;;AAaA,MAAM,sCAAsB,IAAI,QAAkB;AAElD,SAAgB,eACd,YACqB;CACrB,IAAI,SAAS,oBAAoB,IAAI,UAAU;CAC/C,IAAI,CAAC,QAAQ;EACX,SAAS,IAAI,YAAoB;EACjC,oBAAoB,IAAI,YAAY,MAAM;CAC5C;CACA,OAAO;AACT;;;;;;;AAQA,eAAsB,gBACpB,YACA,OACA,OACA,SACe;CACf,MAAM,WAAW,OAAO,CAAC,CAAC,KAAK,OAAO,OAAO,OAAO;CAEpD,IAAI,WAAW,OAAO,eAAe,UAAU;CAE/C,OAAO,YAAY,aAAa,SAAS,WAAW;EAClD,MAAM,SAAS,oBAAoB,IAAI,QAAQ;EAE/C,IAAI,QACF,MAAM,OAAO,KAAK,OAAO,OAAO,OAAO;EAGzC,WAAW,OAAO,eAAe,QAAQ;CAC3C;AACF;AAEA,SAAgB,mBAAmB,YAAuB;CACxD,oBAAoB,OAAO,UAAU;CACrC,yBAAyB,WAAW,IAAI;AAC1C;AAEA,SAAgB,cACd,YACA,OACA,UACY;CACZ,OAAO,WAAW,OAAe,CAAC,CAAC,GAAG,OAAO,QAAQ;AACvD;AAEA,SAAgB,gBACd,YACA,OACA,UACY;CACZ,OAAO,WAAW,OAAe,CAAC,CAAC,KAAK,OAAO,QAAQ;AACzD;AAEA,SAAgB,eACd,YACA,OACA,UACM;CACN,WAAW,OAAe,CAAC,CAAC,IAAI,OAAO,QAAQ;AACjD;AAEA,SAAgB,kBAAsC;CACpD,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"write-methods.d.mts","names":[],"sources":["../../../../../../../../cascade/src/model/methods/write-methods.ts"],"mappings":";;;;KAoBY,iBAAA;;;;;;EAMV,SAAA;;;;;;;;;;;;;;EAeA,IAAI;AAAA"}
1
+ {"version":3,"file":"write-methods.d.mts","names":[],"sources":["../../../../../../../../cascade/src/model/methods/write-methods.ts"],"mappings":";;;;KAqBY,iBAAA;;;;;;EAMV,SAAA;;;;;;;;;;;;;;EAeA,IAAI;AAAA"}