@lenne.tech/nest-server 12.1.1 → 12.3.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 (317) hide show
  1. package/.claude/rules/architecture.md +1 -0
  2. package/.claude/rules/better-auth.md +31 -4
  3. package/.claude/rules/configurable-features.md +8 -6
  4. package/.claude/rules/package-management.md +29 -2
  5. package/.claude/rules/persistence.md +27 -1
  6. package/.claude/rules/testing.md +80 -15
  7. package/.claude/rules/versioning.md +13 -2
  8. package/CLAUDE.md +7 -1
  9. package/FRAMEWORK-API.md +20 -4
  10. package/README.md +20 -2
  11. package/dist/config.env.js +2 -2
  12. package/dist/config.env.js.map +1 -1
  13. package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
  14. package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
  15. package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
  16. package/dist/core/common/interfaces/server-options.interface.d.ts +49 -0
  17. package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
  18. package/dist/core/common/persistence/core-persistence.module.js +12 -2
  19. package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
  20. package/dist/core/common/persistence/data-source.runtime.js +4 -2
  21. package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
  22. package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
  23. package/dist/core/common/persistence/id-format.d.ts +2 -0
  24. package/dist/core/common/persistence/id-format.js +14 -0
  25. package/dist/core/common/persistence/id-format.js.map +1 -1
  26. package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
  27. package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
  28. package/dist/core/common/persistence/model-metadata.registry.js +20 -2
  29. package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
  30. package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
  31. package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
  32. package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
  33. package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
  34. package/dist/core/common/persistence/mongoose-data-store.js +107 -31
  35. package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
  36. package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
  37. package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
  38. package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
  39. package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
  40. package/dist/core/common/persistence/policies/sync.policy.js +77 -0
  41. package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
  42. package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
  43. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
  44. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
  45. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
  46. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
  47. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
  48. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +101 -4
  49. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
  50. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
  51. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
  52. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
  53. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
  54. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
  55. package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
  56. package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
  57. package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
  58. package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
  59. package/dist/core/common/persistence/sql/sql-errors.js +3 -1
  60. package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
  61. package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
  62. package/dist/core/common/persistence/sync-access.registry.js +8 -0
  63. package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
  64. package/dist/core/common/persistence/sync-model.d.ts +29 -0
  65. package/dist/core/common/persistence/sync-model.js +138 -0
  66. package/dist/core/common/persistence/sync-model.js.map +1 -0
  67. package/dist/core/common/services/crud.service.d.ts +13 -1
  68. package/dist/core/common/services/crud.service.js +174 -16
  69. package/dist/core/common/services/crud.service.js.map +1 -1
  70. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
  71. package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
  72. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  73. package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
  74. package/dist/core/modules/ai/core-ai.constants.js +2 -0
  75. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  76. package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
  77. package/dist/core/modules/ai/core-ai.controller.js +31 -1
  78. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  79. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  80. package/dist/core/modules/ai/core-ai.module.js +8 -1
  81. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  82. package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
  83. package/dist/core/modules/ai/core-ai.resolver.js +32 -0
  84. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  85. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
  86. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
  87. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
  88. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
  89. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  90. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
  91. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
  92. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
  93. package/dist/core/modules/ai/index.d.ts +7 -0
  94. package/dist/core/modules/ai/index.js +6 -0
  95. package/dist/core/modules/ai/index.js.map +1 -1
  96. package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
  97. package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
  98. package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
  99. package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
  100. package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
  101. package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
  102. package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
  103. package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
  104. package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
  105. package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
  106. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
  107. package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
  108. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  109. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
  110. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  111. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
  112. package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
  113. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  114. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
  115. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
  116. package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
  117. package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
  118. package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
  119. package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
  120. package/dist/core/modules/ai/services/core-ai.service.js +68 -29
  121. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  122. package/dist/core/modules/better-auth/better-auth.config.js +3 -1
  123. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  124. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
  125. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  126. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
  127. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
  128. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
  129. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
  130. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  131. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  132. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
  133. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  134. package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
  135. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  136. package/dist/core/modules/better-auth/index.d.ts +1 -0
  137. package/dist/core/modules/better-auth/index.js +1 -0
  138. package/dist/core/modules/better-auth/index.js.map +1 -1
  139. package/dist/core/modules/error-code/error-codes.d.ts +72 -0
  140. package/dist/core/modules/error-code/error-codes.js +64 -0
  141. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  142. package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
  143. package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
  144. package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
  145. package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
  146. package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
  147. package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
  148. package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
  149. package/dist/core/modules/sync/core-sync-cursor.js +77 -0
  150. package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
  151. package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
  152. package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
  153. package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
  154. package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
  155. package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
  156. package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
  157. package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
  158. package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
  159. package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
  160. package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
  161. package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
  162. package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
  163. package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
  164. package/dist/core/modules/sync/core-sync-state.store.js +64 -0
  165. package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
  166. package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
  167. package/dist/core/modules/sync/core-sync.constants.js +23 -0
  168. package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
  169. package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
  170. package/dist/core/modules/sync/core-sync.controller.js +79 -0
  171. package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
  172. package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
  173. package/dist/core/modules/sync/core-sync.decorators.js +28 -0
  174. package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
  175. package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
  176. package/dist/core/modules/sync/core-sync.module.js +50 -0
  177. package/dist/core/modules/sync/core-sync.module.js.map +1 -0
  178. package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
  179. package/dist/core/modules/sync/core-sync.resolver.js +46 -0
  180. package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
  181. package/dist/core/modules/sync/core-sync.service.d.ts +143 -0
  182. package/dist/core/modules/sync/core-sync.service.js +1237 -0
  183. package/dist/core/modules/sync/core-sync.service.js.map +1 -0
  184. package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
  185. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
  186. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
  187. package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
  188. package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
  189. package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
  190. package/dist/core/modules/tenant/core-tenant.guard.js +14 -0
  191. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  192. package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
  193. package/dist/core/modules/tenant/core-tenant.service.js +153 -1
  194. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  195. package/dist/core.module.js +14 -0
  196. package/dist/core.module.js.map +1 -1
  197. package/dist/index.d.ts +16 -0
  198. package/dist/index.js +16 -0
  199. package/dist/index.js.map +1 -1
  200. package/dist/server/modules/error-code/error-codes.d.ts +8 -0
  201. package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
  202. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
  203. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
  204. package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
  205. package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
  206. package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
  207. package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
  208. package/dist/server/modules/sync-note/sync-note.model.js +73 -0
  209. package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
  210. package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
  211. package/dist/server/modules/sync-note/sync-note.module.js +21 -0
  212. package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
  213. package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
  214. package/dist/server/modules/sync-note/sync-note.service.js +35 -0
  215. package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
  216. package/dist/test/test.helper.d.ts +1 -0
  217. package/dist/test/test.helper.js +9 -2
  218. package/dist/test/test.helper.js.map +1 -1
  219. package/docs/REQUEST-LIFECYCLE.md +4 -2
  220. package/docs/error-codes.md +14 -0
  221. package/docs/offline-sync-protocol.md +111 -68
  222. package/docs/security-overrides.md +14 -0
  223. package/migration-guides/12.0.x-to-12.1.x.md +0 -1
  224. package/migration-guides/12.1.x-to-12.2.x.md +154 -0
  225. package/migration-guides/12.2.x-to-12.3.x.md +440 -0
  226. package/package.json +23 -21
  227. package/src/config.env.ts +6 -2
  228. package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
  229. package/src/core/common/interfaces/server-options.interface.ts +229 -8
  230. package/src/core/common/interfaces/service-options.interface.ts +15 -0
  231. package/src/core/common/persistence/core-persistence.module.ts +21 -2
  232. package/src/core/common/persistence/data-source.runtime.ts +5 -2
  233. package/src/core/common/persistence/data-store.interface.ts +65 -12
  234. package/src/core/common/persistence/id-format.ts +26 -0
  235. package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
  236. package/src/core/common/persistence/model-metadata.registry.ts +31 -2
  237. package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
  238. package/src/core/common/persistence/mongoose-data-store.ts +171 -35
  239. package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
  240. package/src/core/common/persistence/policies/sync.policy.ts +121 -0
  241. package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
  242. package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
  243. package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +129 -3
  244. package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
  245. package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
  246. package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
  247. package/src/core/common/persistence/sql/sql-errors.ts +5 -1
  248. package/src/core/common/persistence/sync-access.registry.ts +50 -0
  249. package/src/core/common/persistence/sync-model.ts +270 -0
  250. package/src/core/common/services/crud.service.ts +257 -19
  251. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
  252. package/src/core/modules/ai/README.md +163 -25
  253. package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
  254. package/src/core/modules/ai/core-ai.constants.ts +6 -0
  255. package/src/core/modules/ai/core-ai.controller.ts +36 -1
  256. package/src/core/modules/ai/core-ai.module.ts +12 -0
  257. package/src/core/modules/ai/core-ai.resolver.ts +36 -0
  258. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
  259. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
  260. package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
  261. package/src/core/modules/ai/index.ts +18 -0
  262. package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
  263. package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
  264. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
  265. package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
  266. package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
  267. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
  268. package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
  269. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
  270. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
  271. package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
  272. package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
  273. package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
  274. package/src/core/modules/ai/services/core-ai.service.ts +134 -35
  275. package/src/core/modules/api-token/README.md +4 -2
  276. package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
  277. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
  278. package/src/core/modules/better-auth/README.md +32 -1
  279. package/src/core/modules/better-auth/better-auth.config.ts +12 -1
  280. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
  281. package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
  282. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
  283. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
  284. package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
  285. package/src/core/modules/better-auth/index.ts +1 -0
  286. package/src/core/modules/error-code/error-codes.ts +77 -0
  287. package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
  288. package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
  289. package/src/core/modules/sync/README.md +198 -0
  290. package/src/core/modules/sync/SECURITY.md +65 -0
  291. package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
  292. package/src/core/modules/sync/core-sync-cursor.ts +117 -0
  293. package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
  294. package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
  295. package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
  296. package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
  297. package/src/core/modules/sync/core-sync-state.store.ts +89 -0
  298. package/src/core/modules/sync/core-sync.constants.ts +51 -0
  299. package/src/core/modules/sync/core-sync.controller.ts +68 -0
  300. package/src/core/modules/sync/core-sync.decorators.ts +84 -0
  301. package/src/core/modules/sync/core-sync.module.ts +75 -0
  302. package/src/core/modules/sync/core-sync.resolver.ts +35 -0
  303. package/src/core/modules/sync/core-sync.service.ts +1773 -0
  304. package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
  305. package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
  306. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
  307. package/src/core/modules/tenant/README.md +49 -3
  308. package/src/core/modules/tenant/core-tenant.guard.ts +26 -2
  309. package/src/core/modules/tenant/core-tenant.service.ts +287 -2
  310. package/src/core.module.ts +25 -0
  311. package/src/index.ts +21 -0
  312. package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
  313. package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
  314. package/src/server/modules/sync-note/sync-note.model.ts +77 -0
  315. package/src/server/modules/sync-note/sync-note.module.ts +16 -0
  316. package/src/server/modules/sync-note/sync-note.service.ts +25 -0
  317. package/src/test/test.helper.ts +17 -3
@@ -2,6 +2,22 @@ import { Logger } from '@nestjs/common';
2
2
  import type { Model } from 'mongoose';
3
3
 
4
4
  import { getStringIds } from '../helpers/id.helper.js';
5
+ import {
6
+ includeTombstones,
7
+ mongoChangeWindowMs,
8
+ mongoServerNow,
9
+ syncOfSchema,
10
+ tombstoneWrite,
11
+ } from './mongo/sync-schema.plugin.js';
12
+ import type { SyncDescriptor } from './model-descriptor.interface.js';
13
+ import {
14
+ assertClientId,
15
+ insertWithServerId,
16
+ suppliedId,
17
+ syncInsertRecord,
18
+ tombstoneClearedFields,
19
+ } from './policies/sync.policy.js';
20
+ import { SYNC_CLOCK_FIELD, SYNC_DELETED_FIELD } from './sync-model.js';
5
21
  import {
6
22
  assertAccessReason,
7
23
  assertArrayField,
@@ -10,10 +26,13 @@ import {
10
26
  assertSelectFields,
11
27
  } from './store-guards.js';
12
28
  import type {
29
+ ChangeClock,
13
30
  DataStore,
14
31
  DataStoreUpdate,
32
+ DeleteOptions,
15
33
  FindOptions,
16
34
  PushOptions,
35
+ ReadOptions,
17
36
  StoreCapabilities,
18
37
  StoredRecord,
19
38
  UpdateOptions,
@@ -42,18 +61,34 @@ export const MONGOOSE_STORE_CAPABILITIES: Readonly<StoreCapabilities> = Object.f
42
61
  * are the Mongo-operator form — except that the neutral `id` is addressed as `_id`.
43
62
  */
44
63
  export class MongooseDataStore<T = Record<string, any>> implements DataStore<T> {
45
- readonly capabilities = MONGOOSE_STORE_CAPABILITIES;
64
+ readonly capabilities: Readonly<StoreCapabilities>;
46
65
  readonly driver = 'mongodb';
47
66
  protected readonly logger = new Logger(MongooseDataStore.name);
67
+ /** The model's sync declaration (`@PersistedModel({ sync })`), read from its schema */
68
+ protected readonly sync?: SyncDescriptor;
48
69
 
49
- constructor(protected readonly model: Model<any>) {}
70
+ constructor(protected readonly model: Model<any>) {
71
+ this.sync = syncOfSchema(model?.schema);
72
+ this.capabilities = this.sync
73
+ ? Object.freeze({ ...MONGOOSE_STORE_CAPABILITIES, changeOrder: 'clock', idType: this.sync.idFormat })
74
+ : MONGOOSE_STORE_CAPABILITIES;
75
+ }
50
76
 
51
77
  get modelName(): string {
52
78
  return this.model.modelName;
53
79
  }
54
80
 
55
- async count(where?: Where<T>): Promise<number> {
56
- return this.model.countDocuments(toMongoFilter(where)).exec();
81
+ /** The server clock and the window W of a synchronized model (its change order is the clock, plan §3.4) */
82
+ async changeClock(): Promise<ChangeClock> {
83
+ if (!this.sync) {
84
+ throw new Error(`changeClock(): ${this.modelName} is not a synchronized model (@PersistedModel({ sync }))`);
85
+ }
86
+ const [now, windowMs] = await Promise.all([mongoServerNow(this.model.db), mongoChangeWindowMs(this.model.db)]);
87
+ return { now, windowMs };
88
+ }
89
+
90
+ async count(where?: Where<T>, options: ReadOptions = {}): Promise<number> {
91
+ return this.tombstones(this.model.countDocuments(toMongoFilter(where)), options).exec();
57
92
  }
58
93
 
59
94
  async sum(field: string, where?: Where<T>): Promise<{ count: number; sum: number }> {
@@ -69,28 +104,43 @@ export class MongooseDataStore<T = Record<string, any>> implements DataStore<T>
69
104
  return { count: row?.count ?? 0, sum: row?.sum ?? 0 };
70
105
  }
71
106
 
72
- async deleteById(id: string): Promise<null | StoredRecord<T>> {
73
- return toRecord<T>(await this.model.findByIdAndDelete(id).lean().exec());
107
+ async deleteById(id: string, options: DeleteOptions = {}): Promise<null | StoredRecord<T>> {
108
+ if (this.sync && !options.hardDelete) {
109
+ return this.tombstone({ _id: id }, options);
110
+ }
111
+ return toRecord<T>(await this.tombstones(this.model.findByIdAndDelete(id), options).lean().exec());
74
112
  }
75
113
 
76
- async deleteMany(where: Where<T>): Promise<number> {
77
- const result = await this.model.deleteMany(toMongoFilter(where)).exec();
114
+ async deleteMany(where: Where<T>, options: DeleteOptions = {}): Promise<number> {
115
+ if (this.sync && !options.hardDelete) {
116
+ const query = tombstoneWrite(this.model.updateMany(toMongoFilter(where), this.tombstoneUpdate() as any));
117
+ const result = await this.tombstones(query, options).exec();
118
+ return result.matchedCount;
119
+ }
120
+ const result = await this.tombstones(this.model.deleteMany(toMongoFilter(where)), options).exec();
78
121
  return result.deletedCount;
79
122
  }
80
123
 
81
- async deleteOne(where: Where<T>): Promise<null | StoredRecord<T>> {
82
- return toRecord<T>(await this.model.findOneAndDelete(toMongoFilter(where)).lean().exec());
124
+ async deleteOne(where: Where<T>, options: DeleteOptions = {}): Promise<null | StoredRecord<T>> {
125
+ if (this.sync && !options.hardDelete) {
126
+ return this.tombstone(toMongoFilter(where), options);
127
+ }
128
+ return toRecord<T>(
129
+ await this.tombstones(this.model.findOneAndDelete(toMongoFilter(where)), options)
130
+ .lean()
131
+ .exec(),
132
+ );
83
133
  }
84
134
 
85
- async distinct(field: string, where?: Where<T>): Promise<unknown[]> {
86
- return this.model.distinct(field, toMongoFilter(where)).exec();
135
+ async distinct(field: string, where?: Where<T>, options: ReadOptions = {}): Promise<unknown[]> {
136
+ return this.tombstones(this.model.distinct(field, toMongoFilter(where)), options).exec();
87
137
  }
88
138
 
89
139
  async find(where?: Where<T>, options: FindOptions = {}): Promise<StoredRecord<T>[]> {
90
140
  const sample = options.sample === undefined ? undefined : assertSampleSize(options.sample);
91
- let query = this.model.find(toMongoFilter(where));
141
+ let query = this.tombstones(this.model.find(toMongoFilter(where)), options);
92
142
  if (options.select || options.exclude) query = query.select(toProjection(options));
93
- if (options.sort) query = query.sort(options.sort);
143
+ if (options.sort) query = query.sort(toMongoSort(options.sort));
94
144
  if (options.skip !== undefined) query = query.skip(options.skip);
95
145
  if (options.limit !== undefined) query = query.limit(options.limit);
96
146
  if (options.collation) query = query.collation(options.collation as any);
@@ -101,21 +151,27 @@ export class MongooseDataStore<T = Record<string, any>> implements DataStore<T>
101
151
  return sample === undefined ? records : randomSample(records, sample);
102
152
  }
103
153
 
104
- async findById(id: string, options: Pick<FindOptions, 'exclude' | 'select'> = {}): Promise<null | StoredRecord<T>> {
105
- let query = this.model.findById(id);
154
+ async findById(
155
+ id: string,
156
+ options: Pick<FindOptions, 'exclude' | 'includeTombstones' | 'select'> = {},
157
+ ): Promise<null | StoredRecord<T>> {
158
+ let query = this.tombstones(this.model.findById(id), options);
106
159
  if (options.select || options.exclude) query = query.select(toProjection(options));
107
160
  return toRecord<T>(await query.lean().exec());
108
161
  }
109
162
 
110
163
  async findOne(where: Where<T>, options: Omit<FindOptions, 'limit' | 'skip'> = {}): Promise<null | StoredRecord<T>> {
111
- let query = this.model.findOne(toMongoFilter(where));
164
+ let query = this.tombstones(this.model.findOne(toMongoFilter(where)), options);
112
165
  if (options.select || options.exclude) query = query.select(toProjection(options));
113
- if (options.sort) query = query.sort(options.sort);
166
+ if (options.sort) query = query.sort(toMongoSort(options.sort));
114
167
  if (options.collation) query = query.collation(options.collation as any);
115
168
  return toRecord<T>(await query.lean().exec());
116
169
  }
117
170
 
118
171
  async insert(record: Partial<T>): Promise<StoredRecord<T>> {
172
+ if (this.sync) {
173
+ return this.insertSynchronized(record as Record<string, any>);
174
+ }
119
175
  // `create()` runs the save hooks, so tenant stamping, audit fields and password hashing apply
120
176
  const created = await this.model.create(record);
121
177
  return toRecord<T>(created.toObject());
@@ -161,30 +217,95 @@ export class MongooseDataStore<T = Record<string, any>> implements DataStore<T>
161
217
  update: DataStoreUpdate<T>,
162
218
  options: UpdateOptions = {},
163
219
  ): Promise<null | StoredRecord<T>> {
164
- const updated = await this.model
165
- .findByIdAndUpdate(id, update as any, {
166
- returnDocument: 'after',
167
- ...(options.timestamps === false ? { timestamps: false } : {}),
168
- })
169
- .lean()
170
- .exec();
171
- return toRecord<T>(updated);
220
+ const query = this.model.findByIdAndUpdate(id, update as any, {
221
+ returnDocument: 'after',
222
+ ...(options.timestamps === false ? { timestamps: false } : {}),
223
+ });
224
+ return toRecord<T>(await this.tombstones(query, options).lean().exec());
172
225
  }
173
226
 
174
- async updateMany(where: Where<T>, update: DataStoreUpdate<T>): Promise<number> {
175
- const result = await this.model.updateMany(toMongoFilter(where), update as any).exec();
227
+ async updateMany(where: Where<T>, update: DataStoreUpdate<T>, options: UpdateOptions = {}): Promise<number> {
228
+ const query = this.model.updateMany(
229
+ toMongoFilter(where),
230
+ update as any,
231
+ options.timestamps === false ? { timestamps: false } : {},
232
+ );
233
+ const result = await this.tombstones(query, options).exec();
176
234
  return result.matchedCount;
177
235
  }
178
236
 
179
- async updateOne(where: Where<T>, update: DataStoreUpdate<T>): Promise<null | StoredRecord<T>> {
180
- const updated = await this.model
181
- .findOneAndUpdate(toMongoFilter(where), update as any, { returnDocument: 'after' })
182
- .lean()
183
- .exec();
184
- return toRecord<T>(updated);
237
+ async updateOne(
238
+ where: Where<T>,
239
+ update: DataStoreUpdate<T>,
240
+ options: UpdateOptions = {},
241
+ ): Promise<null | StoredRecord<T>> {
242
+ const query = this.model.findOneAndUpdate(toMongoFilter(where), update as any, {
243
+ returnDocument: 'after',
244
+ ...(options.timestamps === false ? { timestamps: false } : {}),
245
+ });
246
+ return toRecord<T>(await this.tombstones(query, options).lean().exec());
247
+ }
248
+
249
+ // -- synchronized models ------------------------------------------------------------------------------------
250
+
251
+ /** A query that also sees tombstones when the caller asked for them (synchronized models only) */
252
+ protected tombstones<Q extends object>(query: Q, options: { includeTombstones?: boolean }): Q {
253
+ return this.sync && options.includeTombstones ? includeTombstones(query) : query;
254
+ }
255
+
256
+ /**
257
+ * The insert of a synchronized model: system fields set by the store, a client id in the model's format kept, a
258
+ * server id generated anew when it is taken (plan §3.3, E6). `create()` runs every save hook, as for any model.
259
+ */
260
+ protected async insertSynchronized(record: Record<string, any>): Promise<StoredRecord<T>> {
261
+ const { _id: _ignored, id: _alsoIgnored, ...doc } = syncInsertRecord(record);
262
+ const supplied = suppliedId(record);
263
+ if (supplied !== undefined && supplied !== null) {
264
+ const created = await this.model.create({
265
+ ...doc,
266
+ _id: assertClientId(supplied, this.sync.idFormat, this.modelName),
267
+ });
268
+ return toRecord<T>(created.toObject());
269
+ }
270
+ const created = await insertWithServerId(
271
+ this.sync.idFormat,
272
+ (id) => this.model.create({ ...doc, _id: id }),
273
+ (error) => isDuplicateIdError(error),
274
+ );
275
+ return toRecord<T>(created.toObject());
276
+ }
277
+
278
+ /** The update that turns a record into a tombstone: content cleared, `deletedAt` from the server clock */
279
+ protected tombstoneUpdate(): Record<string, unknown> {
280
+ const fields = Object.keys(this.model.schema.paths).map((path) => path.split('.', 1)[0]);
281
+ const cleared = tombstoneClearedFields(new Set(fields), this.sync);
282
+ return {
283
+ $currentDate: { [SYNC_CLOCK_FIELD]: true, [SYNC_DELETED_FIELD]: true },
284
+ ...(cleared.length ? { $unset: Object.fromEntries(cleared.map((field) => [field, 1])) } : {}),
285
+ };
286
+ }
287
+
288
+ /** Turn the first record matching `filter` into a tombstone; returns it as it was before */
289
+ protected async tombstone(filter: Record<string, any>, options: DeleteOptions): Promise<null | StoredRecord<T>> {
290
+ const query = tombstoneWrite(
291
+ this.model.findOneAndUpdate(filter, this.tombstoneUpdate() as any, { returnDocument: 'before' }),
292
+ );
293
+ return toRecord<T>(await this.tombstones(query, options).lean().exec());
185
294
  }
186
295
  }
187
296
 
297
+ /** A duplicate-key error raised by the `_id` index — the case a server-generated id is replaced for */
298
+ export function isDuplicateIdError(error: unknown): boolean {
299
+ const candidate = error as { code?: unknown; keyPattern?: Record<string, unknown>; message?: unknown };
300
+ if (candidate?.code !== 11000) {
301
+ return false;
302
+ }
303
+ if (candidate.keyPattern) {
304
+ return Object.keys(candidate.keyPattern).length === 1 && '_id' in candidate.keyPattern;
305
+ }
306
+ return typeof candidate.message === 'string' && /index: _id_ /.test(candidate.message);
307
+ }
308
+
188
309
  /** The neutral `id` is `_id` in MongoDB — at the top level and inside `$and` / `$or` / `$nor`. */
189
310
  export function toMongoFilter(where: undefined | Where<any>): Record<string, any> {
190
311
  if (!where) {
@@ -203,13 +324,28 @@ export function toMongoFilter(where: undefined | Where<any>): Record<string, any
203
324
  return filter;
204
325
  }
205
326
 
327
+ /** The neutral `id` is `_id` in a sort too */
328
+ function toMongoSort(sort: Record<string, -1 | 1>): Record<string, -1 | 1> {
329
+ return Object.fromEntries(
330
+ Object.entries(sort).map(([field, direction]) => [field === 'id' ? '_id' : field, direction]),
331
+ );
332
+ }
333
+
206
334
  /** A lean document as a neutral record: the id as a string under `id`, no `_id`, no version key. */
207
335
  function toRecord<T>(doc: unknown): StoredRecord<T> {
208
336
  if (!doc) {
209
337
  return null;
210
338
  }
211
339
  const { __v: _version, _id, ...fields } = doc as Record<string, any>;
212
- return { ...fields, id: _id === undefined ? fields.id : getStringIds(_id) } as StoredRecord<T>;
340
+ return { ...fields, id: _id === undefined ? fields.id : storedIdString(_id) } as StoredRecord<T>;
341
+ }
342
+
343
+ /** An id as the neutral string: a BSON UUID (a uuidv7 model's id) in its hyphenated form, anything else as before */
344
+ function storedIdString(id: unknown): string {
345
+ if (id && typeof id === 'object' && (id as { sub_type?: unknown }).sub_type === 4) {
346
+ return String(id);
347
+ }
348
+ return getStringIds(id);
213
349
  }
214
350
 
215
351
  function toProjection(options: Pick<FindOptions, 'exclude' | 'select'>): string {
@@ -1,3 +1,5 @@
1
+ import type { SyncModelOptions } from './sync-model.js';
2
+
1
3
  /**
2
4
  * Persistence options a model class declares in code.
3
5
  */
@@ -8,6 +10,13 @@ export interface PersistedModelOptions {
8
10
  * @default the default source
9
11
  */
10
12
  dataSource?: string;
13
+ /**
14
+ * The model is synchronized to offline clients (`docs/offline-sync-protocol.md`): its records carry `version`,
15
+ * `deletedAt` and a change position, a delete leaves a tombstone, and unique indexes apply to live records only.
16
+ * `true` declares it with the defaults. Takes effect for a model registered with `CorePersistenceModule.forFeature()`.
17
+ * @default not synchronized
18
+ */
19
+ sync?: boolean | SyncModelOptions;
11
20
  }
12
21
 
13
22
  const PERSISTED_MODEL_OPTIONS = Symbol('PERSISTED_MODEL_OPTIONS');
@@ -59,3 +68,20 @@ export function getPersistedModelOptionsByName(name: string): PersistedModelOpti
59
68
  }
60
69
  return classes.length ? getPersistedModelOptions(classes[0]) : undefined;
61
70
  }
71
+
72
+ /**
73
+ * The `sync` option declared for the model registered under `name` without its class. Whether a model is synchronized
74
+ * changes what its table holds and what a delete does, so two decorated classes of that name that disagree are never
75
+ * guessed between either.
76
+ */
77
+ export function getPersistedSyncByName(name: string): PersistedModelOptions['sync'] {
78
+ const classes = declaredByName.get(name) ?? [];
79
+ const syncs = new Set(classes.map((target) => JSON.stringify(getPersistedModelOptions(target)?.sync ?? false)));
80
+ if (syncs.size > 1) {
81
+ throw new Error(
82
+ `@PersistedModel: ${classes.length} classes named "${name}" declare different sync options; pass \`model\` ` +
83
+ 'in CorePersistenceModule.forFeature() to say which one this model is',
84
+ );
85
+ }
86
+ return classes.length ? getPersistedModelOptions(classes[0])?.sync : undefined;
87
+ }
@@ -0,0 +1,121 @@
1
+ import { BadRequestException, ConflictException } from '@nestjs/common';
2
+
3
+ import { ErrorCode } from '../../../modules/error-code/error-codes.js';
4
+ import { generateId, generateUuidV7, isUuidV7, isWellFormedId } from '../id-format.js';
5
+ import type { SyncDescriptor, SyncIdFormat } from '../model-descriptor.interface.js';
6
+ import { SYNC_DELETED_FIELD, SYNC_SYSTEM_FIELDS, SYNC_VERSION_FIELD } from '../sync-model.js';
7
+
8
+ /**
9
+ * Write rules of synchronized models, shared by every data store (offline-sync plan §3.3) — like the tenant policy,
10
+ * a fix here reaches every database.
11
+ */
12
+
13
+ /** How often an insert with a SERVER-generated id is tried again when that id is taken (plan §3.3, E6) */
14
+ export const SERVER_ID_ATTEMPTS = 3;
15
+
16
+ /** Fields a tombstone keeps besides the declared scope fields (protocol §2) */
17
+ const TOMBSTONE_KEPT = new Set([
18
+ '_id',
19
+ '__v',
20
+ 'createdAt',
21
+ 'createdBy',
22
+ 'id',
23
+ 'tenantId',
24
+ 'updatedAt',
25
+ 'updatedBy',
26
+ ...SYNC_SYSTEM_FIELDS,
27
+ ]);
28
+
29
+ /**
30
+ * The record a caller inserts, as a synchronized model stores it: the system fields are the store's (`version: 1`,
31
+ * `deletedAt: null`), whatever the caller sent
32
+ */
33
+ export function syncInsertRecord(record: Record<string, any>): Record<string, any> {
34
+ const doc = { ...record };
35
+ for (const field of SYNC_SYSTEM_FIELDS) {
36
+ delete doc[field];
37
+ }
38
+ doc[SYNC_VERSION_FIELD] = 1;
39
+ doc[SYNC_DELETED_FIELD] = null;
40
+ return doc;
41
+ }
42
+
43
+ /** The content fields a delete clears: everything but the id, the system, audit and tenant fields and the scope */
44
+ export function tombstoneClearedFields(fields: Iterable<string>, sync: SyncDescriptor): string[] {
45
+ const scope = new Set(sync.scope);
46
+ return [...fields].filter((field) => !TOMBSTONE_KEPT.has(field) && !scope.has(field));
47
+ }
48
+
49
+ /** A new id in the model's format */
50
+ export function generateSyncId(format: SyncIdFormat): string {
51
+ return format === 'uuidv7' ? generateUuidV7() : generateId();
52
+ }
53
+
54
+ /** Whether a value is an id in the model's format */
55
+ export function isSyncId(value: unknown, format: SyncIdFormat): boolean {
56
+ return format === 'uuidv7' ? isUuidV7(value) : typeof value === 'string' && isWellFormedId(value);
57
+ }
58
+
59
+ /**
60
+ * A client-supplied id, checked against the model's format (protocol §3: `INVALID_ID`). Unlike an ordinary model —
61
+ * which replaces a malformed id with a new one — a synchronized model refuses it: the client keeps its own id and
62
+ * would lose track of the record.
63
+ */
64
+ export function assertClientId(value: unknown, format: SyncIdFormat, model: string): string {
65
+ const id = typeof value === 'string' ? value : value?.toString?.();
66
+ if (format === 'objectId' && typeof id === 'string' && isWellFormedId(id)) {
67
+ return id.toLowerCase();
68
+ }
69
+ if (format === 'uuidv7' && isUuidV7(id)) {
70
+ return id;
71
+ }
72
+ throw new BadRequestException(
73
+ `${ErrorCode.INVALID_FIELD_FORMAT} [id]: ${model} takes ${format === 'uuidv7' ? 'a lowercase uuidv7' : 'a 24-character hex ObjectId'} as id`,
74
+ );
75
+ }
76
+
77
+ /** The id an insert names, if any (`id` or `_id`) */
78
+ export function suppliedId(record: Record<string, any>): unknown {
79
+ return record.id ?? record._id;
80
+ }
81
+
82
+ /**
83
+ * Insert with a server-generated id, generating a fresh one when it is taken (plan §3.3, E6): server ids are
84
+ * predictable from one observed id, so a client could occupy the next ones; a server id was never shown to anyone, so
85
+ * replacing it is invisible. A client-supplied id is never replaced — its duplicate is the caller's answer.
86
+ */
87
+ export async function insertWithServerId<R>(
88
+ format: SyncIdFormat,
89
+ attempt: (id: string) => Promise<R>,
90
+ isIdTaken: (error: unknown) => boolean,
91
+ ): Promise<R> {
92
+ for (let tries = 1; ; tries++) {
93
+ try {
94
+ return await attempt(generateSyncId(format));
95
+ } catch (error) {
96
+ if (tries >= SERVER_ID_ATTEMPTS || !isIdTaken(error)) {
97
+ throw error;
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ /** A conditional change (`expectedVersion`) whose record has another version: the caller resolves and retries */
104
+ export class VersionConflictError extends ConflictException {
105
+ override readonly name = 'VersionConflictError';
106
+
107
+ constructor(
108
+ readonly model: string,
109
+ readonly id: string,
110
+ ) {
111
+ super(`${ErrorCode.VERSION_CONFLICT}: ${model} ${id} was changed in the meantime`);
112
+ }
113
+ }
114
+
115
+ /** Whether `expectedVersion` is a usable version number */
116
+ export function assertExpectedVersion(value: unknown): number {
117
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
118
+ throw new BadRequestException(`${ErrorCode.INVALID_FIELD_FORMAT} [expectedVersion]: a positive integer`);
119
+ }
120
+ return value;
121
+ }