@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
@@ -0,0 +1,89 @@
1
+ import type { DataStore } from '../../common/persistence/data-store.interface.js';
2
+ import { isDuplicateKey } from './core-sync-mutation.store.js';
3
+
4
+ interface StateRecord {
5
+ epoch?: null | number;
6
+ horizon?: Date | null;
7
+ key: string;
8
+ }
9
+
10
+ /**
11
+ * Access epochs and tombstone horizons (offline-sync plan §3.7, §3.8), keyed by string:
12
+ *
13
+ * - `epoch:t:<model>:<tenant>` — bumped when what a tenant's members may see changes (`invalidateTenant()`, a record
14
+ * moved to another tenant). A cursor carries the epochs it was issued under; another value answers 410 `access`.
15
+ * - `epoch:u:<model>:<tenant>:<user>` — bumped when a scope field (`sync.scope`) changes away from that user, so only
16
+ * that user's cursors resync.
17
+ * - `horizon:<model>` — the oldest point a cursor may continue from; a cursor whose state is older answers 410 `horizon`.
18
+ *
19
+ * A bump is one atomic increment; a key that does not exist yet is created with a duplicate-key retry, so two
20
+ * concurrent bumps both count.
21
+ */
22
+ export class CoreSyncStateStore {
23
+ constructor(protected readonly store: DataStore<StateRecord>) {}
24
+
25
+ static tenantEpochKey(model: string, tenantId: string): string {
26
+ return `epoch:t:${model}:${tenantId}`;
27
+ }
28
+
29
+ static userEpochKey(model: string, tenantId: string, userId: string): string {
30
+ return `epoch:u:${model}:${tenantId}:${userId}`;
31
+ }
32
+
33
+ static horizonKey(model: string): string {
34
+ return `horizon:${model}`;
35
+ }
36
+
37
+ /** The current epochs of `keys`; a key never bumped is `0` */
38
+ async epochs(keys: readonly string[]): Promise<Map<string, number>> {
39
+ const result = new Map<string, number>(keys.map((key) => [key, 0]));
40
+ if (!keys.length) {
41
+ return result;
42
+ }
43
+ for (const row of await this.store.find({ key: { $in: [...keys] } }, { select: ['epoch', 'key'] })) {
44
+ result.set(row.key, typeof row.epoch === 'number' ? row.epoch : 0);
45
+ }
46
+ return result;
47
+ }
48
+
49
+ async bump(key: string): Promise<void> {
50
+ if (await this.store.updateOne({ key }, { $inc: { epoch: 1 } })) {
51
+ return;
52
+ }
53
+ try {
54
+ await this.store.insert({ epoch: 1, key });
55
+ } catch (error) {
56
+ if (!isDuplicateKey(error)) {
57
+ throw error;
58
+ }
59
+ // Created concurrently: this bump still counts
60
+ await this.store.updateOne({ key }, { $inc: { epoch: 1 } });
61
+ }
62
+ }
63
+
64
+ /** The tombstone horizon of a model, `undefined` while no purge ran */
65
+ async horizon(model: string): Promise<Date | undefined> {
66
+ const row = await this.store.findOne({ key: CoreSyncStateStore.horizonKey(model) });
67
+ return row?.horizon ? new Date(row.horizon) : undefined;
68
+ }
69
+
70
+ /** Move the horizon forward to `at` — never back, so a slower purge cannot reopen a gap a faster one closed */
71
+ async advanceHorizon(model: string, at: Date): Promise<void> {
72
+ const key = CoreSyncStateStore.horizonKey(model);
73
+ const behind = { $or: [{ horizon: null }, { horizon: { $lt: at } }], key };
74
+ if (await this.store.updateOne(behind, { $set: { horizon: at } })) {
75
+ return;
76
+ }
77
+ if (await this.store.findOne({ key })) {
78
+ return;
79
+ }
80
+ try {
81
+ await this.store.insert({ horizon: at, key });
82
+ } catch (error) {
83
+ if (!isDuplicateKey(error)) {
84
+ throw error;
85
+ }
86
+ await this.store.updateOne(behind, { $set: { horizon: at } });
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Constants of the sync module (offline sync, `docs/offline-sync-protocol.md`). Import-free leaf: tokens and numbers
3
+ * only, so every file of the module may import it without an import cycle.
4
+ */
5
+
6
+ /** The framework model holding one entry per pushed mutation (claim, then outcome; protocol §6.5) */
7
+ export const SYNC_MUTATION_MODEL = 'SyncMutation';
8
+
9
+ /** The framework model holding the access epochs and the tombstone horizons (plan §3.7, §3.8) */
10
+ export const SYNC_STATE_MODEL = 'SyncState';
11
+
12
+ /** Metadata of `@SyncService()` on a project's CrudService class */
13
+ export const SYNC_SERVICE_METADATA = 'lt:sync:service';
14
+
15
+ /** Metadata of `@SyncField()`: field name → strategy, on the model class */
16
+ export const SYNC_FIELD_METADATA = 'lt:sync:fields';
17
+
18
+ /**
19
+ * A mutation claimed this recently is still being written (protocol §6.5): a repeat answers `retry`. An older claim
20
+ * belongs to a request that died between the claim and its outcome, and is taken over.
21
+ */
22
+ export const SYNC_CLAIM_TIMEOUT_MS = 30_000;
23
+
24
+ /** How often a write applied on top of the current state (`lww`, `merge`, `custom`) is tried before `retry` */
25
+ export const SYNC_REAPPLY_ATTEMPTS = 3;
26
+
27
+ /** Defaults of the `sync` configuration (plan §3.10) */
28
+ export const SYNC_DEFAULTS = Object.freeze({
29
+ hintDebounceMs: 250,
30
+ maxBytes: 1_048_576,
31
+ maxItems: 100,
32
+ maxLimit: 1000,
33
+ mutationsDays: 30,
34
+ path: 'sync',
35
+ pullLimit: 200,
36
+ rateLimitPull: { max: 120, windowSeconds: 60 },
37
+ rateLimitPush: { max: 60, windowSeconds: 60 },
38
+ tombstonesDays: 90,
39
+ });
40
+
41
+ /** How often the tombstone purge runs (on one replica per run, under a lease; plan §3.7, E8) */
42
+ /** Tombstones per purge statement: small enough that a statement never holds many row locks at once */
43
+ export const SYNC_PURGE_BATCH = 1000;
44
+
45
+ export const SYNC_PURGE_INTERVAL_MS = 60 * 60_000;
46
+
47
+ /** How often an open hint stream sends a comment, so proxies and load balancers keep it open */
48
+ export const SYNC_HINT_KEEPALIVE_MS = 25_000;
49
+
50
+ /** The hard upper bound of `sync.pull.maxLimit` (protocol §5.1) */
51
+ export const SYNC_PULL_LIMIT_CEILING = 1000;
@@ -0,0 +1,68 @@
1
+ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, Req, Res } from '@nestjs/common';
2
+ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
3
+
4
+ import { CurrentUser } from '../../common/decorators/current-user.decorator.js';
5
+ import { Roles } from '../../common/decorators/roles.decorator.js';
6
+ import { RoleEnum } from '../../common/enums/role.enum.js';
7
+ import { CoreSyncBaseController } from './core-sync-base.controller.js';
8
+ import { SYNC_DEFAULTS } from './core-sync.constants.js';
9
+ import { SyncResyncRequiredException } from './core-sync.service.js';
10
+ import type { ISyncPullResult, ISyncPushResult } from './interfaces/core-sync.interfaces.js';
11
+
12
+ /**
13
+ * The sync routes (protocol `docs/offline-sync-protocol.md` §4–§6): `GET /sync/{model}` pulls a page of changes,
14
+ * `POST /sync/{model}` pushes changes. Both need a signed-in caller — a session cookie or a bearer token, exactly as on
15
+ * every other route; the model's own operation roles decide the rest (`@PersistedModel({ sync: { roles } })`). The
16
+ * change hints (`GET /sync/hints`) are `CoreSyncHintsController`'s, registered before this one.
17
+ *
18
+ * API tokens are refused here like on every route without `@ApiTokenScopes()`. A project that wants them extends the
19
+ * controller, re-declares the handlers with `@ApiTokenScopes('sync')` and passes it as `sync.controller` override.
20
+ */
21
+ @ApiTags('sync')
22
+ @Controller(SYNC_DEFAULTS.path)
23
+ @Roles(RoleEnum.S_USER)
24
+ export class CoreSyncController extends CoreSyncBaseController {
25
+ @ApiBearerAuth()
26
+ @ApiOperation({ description: 'One page of changes after the cursor (protocol §5)', summary: 'Pull changes' })
27
+ @Get(':model')
28
+ @Roles(RoleEnum.S_USER)
29
+ async pull(
30
+ @Param('model') model: string,
31
+ @Query('cursor') cursor: string,
32
+ @Query('limit') limit: string,
33
+ @CurrentUser() currentUser: any,
34
+ @Req() req: any,
35
+ @Res({ passthrough: true }) res: any,
36
+ ): Promise<ISyncPullResult | object> {
37
+ await this.limit('pull', currentUser, res);
38
+ await this.scopeTenants(req, currentUser);
39
+ try {
40
+ return await this.syncService.pull(model, { cursor, limit }, currentUser);
41
+ } catch (error) {
42
+ if (error instanceof SyncResyncRequiredException) {
43
+ // The protocol's body (protocol §8), not the shape of whatever exception filter the project registered
44
+ res?.status?.(error.getStatus());
45
+ return error.getResponse() as object;
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ @ApiBearerAuth()
52
+ @ApiOperation({ description: 'Apply pushed changes item by item (protocol §6)', summary: 'Push changes' })
53
+ @HttpCode(HttpStatus.OK)
54
+ @Post(':model')
55
+ @Roles(RoleEnum.S_USER)
56
+ async push(
57
+ @Param('model') model: string,
58
+ @Body() body: any,
59
+ @CurrentUser() currentUser: any,
60
+ @Req() req: any,
61
+ @Res({ passthrough: true }) res: any,
62
+ ): Promise<{ results: ISyncPushResult[] }> {
63
+ this.syncService.assertPushRequest(req);
64
+ await this.limit('push', currentUser, res);
65
+ await this.scopeTenants(req, currentUser);
66
+ return this.syncService.push(model, body, currentUser);
67
+ }
68
+ }
@@ -0,0 +1,84 @@
1
+ import type { Type } from '@nestjs/common';
2
+
3
+ import { SYNC_FIELD_METADATA, SYNC_SERVICE_METADATA } from './core-sync.constants.js';
4
+
5
+ /** What `@SyncService()` declares: the model it serves under `/sync` and the inputs a push is validated against */
6
+ export interface SyncServiceOptions {
7
+ /** Input of a push that creates a record — validated and whitelisted like a request body */
8
+ createInput: Type<any>;
9
+ /** The model class, declared with `@PersistedModel({ sync })` */
10
+ model: Type<any>;
11
+ /**
12
+ * The name the model is registered under in `CorePersistenceModule.forFeature()`, which is also its name in the sync
13
+ * path (`/sync/{model}`)
14
+ * @default the model class's name
15
+ */
16
+ modelName?: string;
17
+ /** Input of a push that changes a record */
18
+ updateInput: Type<any>;
19
+ }
20
+
21
+ /**
22
+ * Offer a model to offline clients through the project's CrudService: `GET/POST /sync/{model}` read and write through
23
+ * this service — its `process()` pipeline, `securityCheck()`, the tenant policy — with the operation roles the model
24
+ * declares in `@PersistedModel({ sync: { roles } })`. Needs the `sync` configuration; without it nothing is registered.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * @SyncService({ createInput: NoteCreateInput, model: Note, updateInput: NoteInput })
29
+ * @Injectable()
30
+ * export class NoteService extends CrudService<Note, NoteCreateInput, NoteInput> { … }
31
+ * ```
32
+ */
33
+ export function SyncService(options: SyncServiceOptions): ClassDecorator {
34
+ return (target) => {
35
+ Reflect.defineMetadata(SYNC_SERVICE_METADATA, { ...options }, target);
36
+ };
37
+ }
38
+
39
+ /** The `@SyncService()` declaration of a service class (inherited from a parent class), if any */
40
+ export function getSyncServiceOptions(target: Function): SyncServiceOptions | undefined {
41
+ return Reflect.getMetadata(SYNC_SERVICE_METADATA, target);
42
+ }
43
+
44
+ /**
45
+ * How a field takes part in a `merge` (protocol §6.4): `lww` — the client's value wins when it changed the field;
46
+ * `reject` — a client change of this field still conflicts when the record changed in the meantime.
47
+ */
48
+ export type SyncFieldStrategy = 'lww' | 'reject';
49
+
50
+ /**
51
+ * Declare how a field is merged when the model's strategy is `merge`. Fields without the decorator merge as `lww`.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * @SyncField('reject')
56
+ * @UnifiedField({ description: 'Price, never merged silently' })
57
+ * price?: number;
58
+ * ```
59
+ */
60
+ export function SyncField(strategy: SyncFieldStrategy): PropertyDecorator {
61
+ if (strategy !== 'lww' && strategy !== 'reject') {
62
+ throw new Error(`@SyncField: strategy must be 'lww' or 'reject', got ${JSON.stringify(strategy)}`);
63
+ }
64
+ return (target, property) => {
65
+ const own: Record<string, SyncFieldStrategy> = Reflect.hasOwnMetadata(SYNC_FIELD_METADATA, target.constructor)
66
+ ? Reflect.getOwnMetadata(SYNC_FIELD_METADATA, target.constructor)
67
+ : {};
68
+ Reflect.defineMetadata(SYNC_FIELD_METADATA, { ...own, [String(property)]: strategy }, target.constructor);
69
+ };
70
+ }
71
+
72
+ /** The `@SyncField()` strategies of a model class, its parents' included (a child's declaration wins) */
73
+ export function getSyncFieldStrategies(modelClass: Function): Record<string, SyncFieldStrategy> {
74
+ const chain: Function[] = [];
75
+ for (let current = modelClass; current && current !== Function.prototype; current = Object.getPrototypeOf(current)) {
76
+ chain.unshift(current);
77
+ }
78
+ return Object.assign(
79
+ {},
80
+ ...chain.map((current) =>
81
+ Reflect.hasOwnMetadata(SYNC_FIELD_METADATA, current) ? Reflect.getOwnMetadata(SYNC_FIELD_METADATA, current) : {},
82
+ ),
83
+ );
84
+ }
@@ -0,0 +1,75 @@
1
+ import { DynamicModule, Module, Type } from '@nestjs/common';
2
+ import { PATH_METADATA } from '@nestjs/common/constants';
3
+ import { DiscoveryModule } from '@nestjs/core';
4
+
5
+ import { CorePersistenceModule } from '../../common/persistence/core-persistence.module.js';
6
+ import { SYNC_DEFAULTS, SYNC_MUTATION_MODEL, SYNC_STATE_MODEL } from './core-sync.constants.js';
7
+ import { CoreSyncController } from './core-sync.controller.js';
8
+ import { CoreSyncHintsController } from './core-sync-hints.controller.js';
9
+ import { CoreSyncHintService } from './core-sync-hints.service.js';
10
+ import { SyncMutationSchema, SyncStateSchema } from './core-sync-infrastructure.models.js';
11
+ import { CoreSyncResolver } from './core-sync.resolver.js';
12
+ import { CoreSyncService } from './core-sync.service.js';
13
+
14
+ /** Options for `CoreSyncModule.forRoot()` */
15
+ export interface CoreSyncModuleOptions {
16
+ /** Custom controller (must extend CoreSyncController) */
17
+ controller?: Type<CoreSyncController>;
18
+ /**
19
+ * Register the GraphQL hint subscription `syncHint` — `false` where GraphQL is disabled or `sync.hints.graphql` /
20
+ * `sync.hints` is `false`
21
+ * @default true
22
+ */
23
+ graphQlHints?: boolean;
24
+ /** Custom controller of `GET /sync/hints` (must extend CoreSyncHintsController) */
25
+ hintsController?: Type<CoreSyncHintsController>;
26
+ /** Route prefix (`sync.path`) @default 'sync' */
27
+ path?: string;
28
+ /** Custom resolver of the hint subscription (must extend CoreSyncResolver) */
29
+ resolver?: Type<CoreSyncResolver>;
30
+ /** Custom service (must extend CoreSyncService) */
31
+ service?: Type<CoreSyncService>;
32
+ }
33
+
34
+ /**
35
+ * Offline sync (`sync` config, protocol `docs/offline-sync-protocol.md`).
36
+ *
37
+ * Registered automatically by `CoreModule.forRoot()` when `sync` is configured. Provides the sync routes, the change
38
+ * hints (SSE and, with GraphQL, the `syncHint` subscription), the service, the tombstone purge and the module's two
39
+ * infrastructure models (mutations; epochs and horizons), which follow `database` like every
40
+ * other model. Which models are offered is decided by the project: a CrudService with `@SyncService()` whose model is
41
+ * declared with `@PersistedModel({ sync })`.
42
+ */
43
+ @Module({})
44
+ export class CoreSyncModule {
45
+ static forRoot(options: CoreSyncModuleOptions = {}): DynamicModule {
46
+ const controller = options.controller ?? CoreSyncController;
47
+ const hintsController = options.hintsController ?? CoreSyncHintsController;
48
+ // The configured prefix wins over the class's declaration, as for the TUS routes
49
+ for (const routes of [hintsController, controller]) {
50
+ Reflect.defineMetadata(PATH_METADATA, options.path || SYNC_DEFAULTS.path, routes);
51
+ }
52
+ return {
53
+ // The hints first: Nest registers routes in this order, and `GET /sync/hints` must win over `GET /sync/:model`
54
+ // whatever a project's controller re-declares
55
+ controllers: [hintsController, controller],
56
+ exports: [CoreSyncHintService, CoreSyncService],
57
+ // Global, as the tenant and API-token modules: a project's own service injects CoreSyncService to report writes
58
+ // past the CrudService (`notifyChange()`, `bumpAccessEpoch()`), and CoreModule does not re-export its imports
59
+ global: true,
60
+ imports: [
61
+ DiscoveryModule,
62
+ CorePersistenceModule.forFeature([
63
+ { name: SYNC_MUTATION_MODEL, schema: SyncMutationSchema },
64
+ { name: SYNC_STATE_MODEL, schema: SyncStateSchema },
65
+ ]),
66
+ ],
67
+ module: CoreSyncModule,
68
+ providers: [
69
+ CoreSyncHintService,
70
+ options.service ? { provide: CoreSyncService, useClass: options.service } : CoreSyncService,
71
+ ...(options.graphQlHints === false ? [] : [options.resolver ?? CoreSyncResolver]),
72
+ ],
73
+ };
74
+ }
75
+ }
@@ -0,0 +1,35 @@
1
+ import { Args, Resolver, Subscription } from '@nestjs/graphql';
2
+
3
+ import { GraphQLUser } from '../../common/decorators/graphql-user.decorator.js';
4
+ import { Roles } from '../../common/decorators/roles.decorator.js';
5
+ import { RoleEnum } from '../../common/enums/role.enum.js';
6
+ import { CoreSyncService } from './core-sync.service.js';
7
+ import { CoreSyncHintModel } from './models/core-sync-hint.model.js';
8
+
9
+ /**
10
+ * The GraphQL hint channel of offline sync (protocol §9): `subscription { syncHint(models: ["Note"]) { model at } }`
11
+ * (`models` optional), registered when
12
+ * GraphQL is enabled and `sync.hints.graphql` is not `false`. Hints reach only subscribers with the model's pull roles,
13
+ * for changes in their own tenants; the WebSocket it runs on is re-checked by `CoreModule`, so changed rights close it.
14
+ *
15
+ * Extend it and pass it as `CoreModule.forRoot(env, { sync: { resolver } })` to change the channel — re-declare
16
+ * `@Subscription()` and `@Roles()` on an override, as on every resolver.
17
+ */
18
+ @Resolver()
19
+ @Roles(RoleEnum.S_USER)
20
+ export class CoreSyncResolver {
21
+ constructor(protected readonly syncService: CoreSyncService) {}
22
+
23
+ @Roles(RoleEnum.S_USER)
24
+ @Subscription(() => CoreSyncHintModel, {
25
+ description: 'A hint whenever a synchronized model the subscriber may pull changes',
26
+ name: 'syncHint',
27
+ })
28
+ syncHint(
29
+ @GraphQLUser() currentUser: any,
30
+ @Args('models', { description: 'Only these models (all when absent)', nullable: true, type: () => [String] })
31
+ models?: string[],
32
+ ): Promise<AsyncIterableIterator<{ syncHint: CoreSyncHintModel }>> {
33
+ return this.syncService.hintIterator(currentUser, models);
34
+ }
35
+ }