@lenne.tech/nest-server 12.1.1 → 12.2.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 (191) hide show
  1. package/.claude/rules/architecture.md +1 -0
  2. package/.claude/rules/configurable-features.md +1 -0
  3. package/.claude/rules/package-management.md +16 -1
  4. package/.claude/rules/persistence.md +26 -0
  5. package/.claude/rules/testing.md +79 -14
  6. package/CLAUDE.md +2 -1
  7. package/FRAMEWORK-API.md +17 -2
  8. package/README.md +18 -0
  9. package/dist/core/common/interfaces/server-options.interface.d.ts +41 -0
  10. package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
  11. package/dist/core/common/persistence/core-persistence.module.js +12 -2
  12. package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
  13. package/dist/core/common/persistence/data-source.runtime.js +4 -2
  14. package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
  15. package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
  16. package/dist/core/common/persistence/id-format.d.ts +2 -0
  17. package/dist/core/common/persistence/id-format.js +14 -0
  18. package/dist/core/common/persistence/id-format.js.map +1 -1
  19. package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
  20. package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
  21. package/dist/core/common/persistence/model-metadata.registry.js +20 -2
  22. package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
  23. package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
  24. package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
  25. package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
  26. package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
  27. package/dist/core/common/persistence/mongoose-data-store.js +107 -31
  28. package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
  29. package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
  30. package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
  31. package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
  32. package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
  33. package/dist/core/common/persistence/policies/sync.policy.js +77 -0
  34. package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
  35. package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
  36. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
  37. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
  38. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
  39. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
  40. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
  41. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +101 -4
  42. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
  43. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
  44. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
  45. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
  46. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
  47. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
  48. package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
  49. package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
  50. package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
  51. package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
  52. package/dist/core/common/persistence/sql/sql-errors.js +3 -1
  53. package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
  54. package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
  55. package/dist/core/common/persistence/sync-access.registry.js +8 -0
  56. package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
  57. package/dist/core/common/persistence/sync-model.d.ts +29 -0
  58. package/dist/core/common/persistence/sync-model.js +138 -0
  59. package/dist/core/common/persistence/sync-model.js.map +1 -0
  60. package/dist/core/common/services/crud.service.d.ts +13 -1
  61. package/dist/core/common/services/crud.service.js +174 -16
  62. package/dist/core/common/services/crud.service.js.map +1 -1
  63. package/dist/core/modules/error-code/error-codes.d.ts +54 -0
  64. package/dist/core/modules/error-code/error-codes.js +48 -0
  65. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  66. package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
  67. package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
  68. package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
  69. package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
  70. package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
  71. package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
  72. package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
  73. package/dist/core/modules/sync/core-sync-cursor.js +77 -0
  74. package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
  75. package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
  76. package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
  77. package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
  78. package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
  79. package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
  80. package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
  81. package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
  82. package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
  83. package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
  84. package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
  85. package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
  86. package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
  87. package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
  88. package/dist/core/modules/sync/core-sync-state.store.js +64 -0
  89. package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
  90. package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
  91. package/dist/core/modules/sync/core-sync.constants.js +23 -0
  92. package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
  93. package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
  94. package/dist/core/modules/sync/core-sync.controller.js +79 -0
  95. package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
  96. package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
  97. package/dist/core/modules/sync/core-sync.decorators.js +28 -0
  98. package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
  99. package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
  100. package/dist/core/modules/sync/core-sync.module.js +50 -0
  101. package/dist/core/modules/sync/core-sync.module.js.map +1 -0
  102. package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
  103. package/dist/core/modules/sync/core-sync.resolver.js +46 -0
  104. package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
  105. package/dist/core/modules/sync/core-sync.service.d.ts +142 -0
  106. package/dist/core/modules/sync/core-sync.service.js +1208 -0
  107. package/dist/core/modules/sync/core-sync.service.js.map +1 -0
  108. package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
  109. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
  110. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
  111. package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
  112. package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
  113. package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
  114. package/dist/core/modules/tenant/core-tenant.guard.js +4 -0
  115. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  116. package/dist/core.module.js +14 -0
  117. package/dist/core.module.js.map +1 -1
  118. package/dist/index.d.ts +16 -0
  119. package/dist/index.js +16 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/server/modules/error-code/error-codes.d.ts +6 -0
  122. package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
  123. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
  124. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
  125. package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
  126. package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
  127. package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
  128. package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
  129. package/dist/server/modules/sync-note/sync-note.model.js +73 -0
  130. package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
  131. package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
  132. package/dist/server/modules/sync-note/sync-note.module.js +21 -0
  133. package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
  134. package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
  135. package/dist/server/modules/sync-note/sync-note.service.js +35 -0
  136. package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
  137. package/docs/REQUEST-LIFECYCLE.md +1 -0
  138. package/docs/offline-sync-protocol.md +96 -62
  139. package/docs/security-overrides.md +14 -0
  140. package/migration-guides/12.1.x-to-12.2.x.md +154 -0
  141. package/package.json +4 -2
  142. package/src/core/common/interfaces/server-options.interface.ts +149 -0
  143. package/src/core/common/interfaces/service-options.interface.ts +15 -0
  144. package/src/core/common/persistence/core-persistence.module.ts +21 -2
  145. package/src/core/common/persistence/data-source.runtime.ts +5 -2
  146. package/src/core/common/persistence/data-store.interface.ts +65 -12
  147. package/src/core/common/persistence/id-format.ts +26 -0
  148. package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
  149. package/src/core/common/persistence/model-metadata.registry.ts +31 -2
  150. package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
  151. package/src/core/common/persistence/mongoose-data-store.ts +171 -35
  152. package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
  153. package/src/core/common/persistence/policies/sync.policy.ts +121 -0
  154. package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
  155. package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
  156. package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +129 -3
  157. package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
  158. package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
  159. package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
  160. package/src/core/common/persistence/sql/sql-errors.ts +5 -1
  161. package/src/core/common/persistence/sync-access.registry.ts +50 -0
  162. package/src/core/common/persistence/sync-model.ts +270 -0
  163. package/src/core/common/services/crud.service.ts +257 -19
  164. package/src/core/modules/error-code/error-codes.ts +59 -0
  165. package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
  166. package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
  167. package/src/core/modules/sync/README.md +188 -0
  168. package/src/core/modules/sync/SECURITY.md +65 -0
  169. package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
  170. package/src/core/modules/sync/core-sync-cursor.ts +117 -0
  171. package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
  172. package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
  173. package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
  174. package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
  175. package/src/core/modules/sync/core-sync-state.store.ts +89 -0
  176. package/src/core/modules/sync/core-sync.constants.ts +51 -0
  177. package/src/core/modules/sync/core-sync.controller.ts +68 -0
  178. package/src/core/modules/sync/core-sync.decorators.ts +84 -0
  179. package/src/core/modules/sync/core-sync.module.ts +75 -0
  180. package/src/core/modules/sync/core-sync.resolver.ts +35 -0
  181. package/src/core/modules/sync/core-sync.service.ts +1728 -0
  182. package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
  183. package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
  184. package/src/core/modules/tenant/core-tenant.guard.ts +8 -0
  185. package/src/core.module.ts +25 -0
  186. package/src/index.ts +21 -0
  187. package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
  188. package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
  189. package/src/server/modules/sync-note/sync-note.model.ts +77 -0
  190. package/src/server/modules/sync-note/sync-note.module.ts +16 -0
  191. package/src/server/modules/sync-note/sync-note.service.ts +25 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The wire format of the sync protocol (`docs/offline-sync-protocol.md`) and the hook of the `custom` strategy.
3
+ * Types only.
4
+ */
5
+
6
+ /** A pushed change (protocol §6.2) */
7
+ export interface ISyncPushItem {
8
+ /** The version the change is based on; absent only on the item that creates the record */
9
+ baseVersion?: number;
10
+ /** The fields to set (`upsert`); validated against the model's input like any create/update */
11
+ data?: Record<string, unknown>;
12
+ /** For `merge`: the field names the client changed. Without it, every key in `data` counts as changed */
13
+ fields?: string[];
14
+ /** The record's id, in the model's format */
15
+ id: string;
16
+ /** Unique per change, generated by the client; the same change sent again carries the same id */
17
+ mutationId: string;
18
+ op: SyncPushOp;
19
+ }
20
+
21
+ export type SyncPushOp = 'delete' | 'upsert';
22
+
23
+ /** Protocol §7 */
24
+ export type SyncItemStatus = 'applied' | 'conflict' | 'merged' | 'not_found' | 'rejected' | 'retry';
25
+
26
+ /** Protocol §7 — the reason of a `rejected` item */
27
+ export type SyncRejectCode = 'CUSTOM_REFUSED' | 'FORBIDDEN' | 'ID_UNAVAILABLE' | 'INVALID_ID' | 'VALIDATION_FAILED';
28
+
29
+ /**
30
+ * A record as a pull delivers it (protocol §5.2): a live record carries its content in `data`, a tombstone carries
31
+ * no `data` — only its id, version, times, `tenantId` and the model's scope fields (protocol §2).
32
+ */
33
+ export interface ISyncRecord {
34
+ [scopeField: string]: unknown;
35
+ data?: Record<string, unknown>;
36
+ deletedAt: Date | null;
37
+ id: string;
38
+ updatedAt?: Date;
39
+ version: number;
40
+ }
41
+
42
+ /** Protocol §6.6 */
43
+ export interface ISyncPushResult {
44
+ code?: SyncRejectCode;
45
+ details?: unknown;
46
+ id: string;
47
+ mutationId: string;
48
+ /** The server's record (`conflict`, `merged`) */
49
+ record?: ISyncRecord;
50
+ status: SyncItemStatus;
51
+ version?: number;
52
+ }
53
+
54
+ /** Protocol §5.2 */
55
+ export interface ISyncPullResult {
56
+ cursor: string;
57
+ hasMore: boolean;
58
+ items: ISyncRecord[];
59
+ }
60
+
61
+ /** Protocol §8 */
62
+ export type SyncResyncReason = 'access' | 'horizon' | 'invalid';
63
+
64
+ /** What `resolveSyncConflict()` receives under the `custom` strategy */
65
+ export interface ISyncConflictContext {
66
+ /** What the client sent */
67
+ change: { baseVersion?: number; data?: Record<string, unknown>; fields?: string[]; op: SyncPushOp };
68
+ /** The authenticated caller, as the controller received it */
69
+ currentUser: any;
70
+ id: string;
71
+ model: string;
72
+ /** The record as the caller may see it now, a live record (a tombstone always conflicts) */
73
+ server: ISyncRecord;
74
+ }
75
+
76
+ /**
77
+ * The answer of `resolveSyncConflict()`: the fields to write on top of the server's record, a deletion, or a refusal
78
+ * (`rejected` / `CUSTOM_REFUSED`). The write runs through the same pipeline as every push.
79
+ */
80
+ export type SyncConflictResolution = { data: Record<string, unknown> } | { delete: true } | { refuse: true };
81
+
82
+ /** A model service whose model declares `strategy: 'custom'` implements this */
83
+ export interface SyncConflictResolver {
84
+ resolveSyncConflict(context: ISyncConflictContext): Promise<SyncConflictResolution> | SyncConflictResolution;
85
+ }
@@ -0,0 +1,18 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../../common/decorators/restricted.decorator.js';
4
+ import { RoleEnum } from '../../../common/enums/role.enum.js';
5
+
6
+ /**
7
+ * A change hint of offline sync (protocol §9): the model changed at this time — nothing else, no record, no id, no
8
+ * tenant. A client answers it with a pull. Who receives it was decided before it was sent.
9
+ */
10
+ @ObjectType('SyncHint', { description: 'A synchronized model changed: pull it' })
11
+ @Restricted(RoleEnum.S_EVERYONE)
12
+ export class CoreSyncHintModel {
13
+ @Field(() => Date, { description: 'When the change was sent' })
14
+ at: Date;
15
+
16
+ @Field(() => String, { description: 'The changed model, as in GET /sync/{model}' })
17
+ model: string;
18
+ }
@@ -26,6 +26,7 @@ import {
26
26
  import { DataSourceConfigError } from '../../common/persistence/data-source.config.js';
27
27
  import type { DataStore } from '../../common/persistence/data-store.interface.js';
28
28
  import { getStoreToken } from '../../common/persistence/persistence.tokens.js';
29
+ import { getSyncAccessListener } from '../../common/persistence/sync-access.registry.js';
29
30
  import { ConfigService } from '../../common/services/config.service.js';
30
31
  import { ResolvedTenantContext, setTenantContextResolver } from '../../common/services/core-tenant-context.registry.js';
31
32
  import { CoreRedisService } from '../../common/services/core-redis.service.js';
@@ -383,6 +384,13 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
383
384
  invalidateTenant(tenantId: string): void {
384
385
  this.clearTenant(tenantId);
385
386
  this.publishInvalidation({ scope: 'tenant', tenantId });
387
+ // Offline sync: the tenant's members resync on their next pull (a no-op without the sync module). Once, here —
388
+ // the epoch lives in the database, so a replica receiving the broadcast must not bump it again
389
+ void getSyncAccessListener()
390
+ ?.tenantInvalidated(tenantId)
391
+ .catch((error) =>
392
+ this.logger.warn(`Sync access epoch of tenant ${tenantId} not bumped: ${(error as Error)?.message ?? error}`),
393
+ );
386
394
  }
387
395
 
388
396
  /**
@@ -84,6 +84,7 @@ import { isHubEnabled, isHubQueriesEnabled } from './core/modules/hub/hub-config
84
84
  import { CorePermissionsModule } from './core/modules/permissions/core-permissions.module.js';
85
85
  import { CoreSystemSetupModule } from './core/modules/system-setup/core-system-setup.module.js';
86
86
  import { CoreApiTokenModule } from './core/modules/api-token/core-api-token.module.js';
87
+ import { CoreSyncModule } from './core/modules/sync/core-sync.module.js';
87
88
  import { CoreTenantModule } from './core/modules/tenant/core-tenant.module.js';
88
89
 
89
90
  /** Configuration warnings of `forRoot()`, through Nest's logger so log levels and a project's logger apply. */
@@ -676,6 +677,30 @@ export class CoreModule implements NestModule {
676
677
  imports.push(CoreApiTokenModule.forRoot({ ...overrides?.apiToken }));
677
678
  }
678
679
 
680
+ // Add CoreSyncModule when sync is configured (boolean shorthand, presence implies enabled). Without it nothing of
681
+ // offline sync is registered: no route, no infrastructure model, no listener on the write paths.
682
+ const syncConfig = config.sync;
683
+ if (
684
+ syncConfig === true ||
685
+ (typeof syncConfig === 'object' && syncConfig !== null && syncConfig.enabled !== false)
686
+ ) {
687
+ imports.push(
688
+ CoreSyncModule.forRoot({
689
+ ...overrides?.sync,
690
+ // The subscription needs GraphQL, and is switched off with its channel or with all hints
691
+ graphQlHints:
692
+ config.graphQl !== false &&
693
+ !(
694
+ typeof syncConfig === 'object' &&
695
+ (syncConfig.hints === false ||
696
+ (typeof syncConfig.hints === 'object' &&
697
+ (syncConfig.hints.graphql === false || syncConfig.hints.enabled === false)))
698
+ ),
699
+ path: typeof syncConfig === 'object' ? syncConfig.path : undefined,
700
+ }),
701
+ );
702
+ }
703
+
679
704
  // Set exports
680
705
  const exports: any[] = [
681
706
  ConfigService,
package/src/index.ts CHANGED
@@ -98,6 +98,8 @@ export * from './core/common/persistence/mongoose-data-store.js';
98
98
  export * from './core/common/persistence/persisted-model.decorator.js';
99
99
  export * from './core/common/persistence/persistence.tokens.js';
100
100
  export * from './core/common/persistence/persistence-runtime.registry.js';
101
+ export { VersionConflictError } from './core/common/persistence/policies/sync.policy.js';
102
+ export * from './core/common/persistence/sync-model.js';
101
103
  export * from './core/common/persistence/unsupported-capability.error.js';
102
104
  export * from './core/common/persistence/sql/mysql/mysql-data-store.js';
103
105
  export * from './core/common/persistence/sql/mysql/mysql-lock-store.js';
@@ -276,6 +278,25 @@ export * from './core/modules/system-setup/core-system-setup.controller.js';
276
278
  export * from './core/modules/system-setup/core-system-setup.module.js';
277
279
  export * from './core/modules/system-setup/core-system-setup.service.js';
278
280
 
281
+ // =====================================================================================================================
282
+ // Core - Modules - Sync (offline sync)
283
+ // =====================================================================================================================
284
+
285
+ export * from './core/modules/sync/core-sync-base.controller.js';
286
+ export * from './core/modules/sync/core-sync.constants.js';
287
+ export * from './core/modules/sync/core-sync.controller.js';
288
+ export * from './core/modules/sync/core-sync.decorators.js';
289
+ export * from './core/modules/sync/core-sync-hints.controller.js';
290
+ export * from './core/modules/sync/core-sync-hints.service.js';
291
+ export * from './core/modules/sync/core-sync-infrastructure.models.js';
292
+ export * from './core/modules/sync/core-sync.module.js';
293
+ export * from './core/modules/sync/core-sync-mutation.store.js';
294
+ export * from './core/modules/sync/core-sync.resolver.js';
295
+ export * from './core/modules/sync/core-sync.service.js';
296
+ export * from './core/modules/sync/core-sync-state.store.js';
297
+ export * from './core/modules/sync/interfaces/core-sync.interfaces.js';
298
+ export * from './core/modules/sync/models/core-sync-hint.model.js';
299
+
279
300
  // =====================================================================================================================
280
301
  // Core - Modules - Tus
281
302
  // =====================================================================================================================
@@ -0,0 +1,14 @@
1
+ import { InputType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../../../core/common/decorators/restricted.decorator.js';
4
+ import { UnifiedField } from '../../../../core/common/decorators/unified-field.decorator.js';
5
+ import { RoleEnum } from '../../../../core/common/enums/role.enum.js';
6
+ import { SyncNoteInput } from './sync-note.input.js';
7
+
8
+ /** Input of a pushed note that creates it; the client chose its id (protocol §3) */
9
+ @InputType({ description: 'Input to create a synchronized note' })
10
+ @Restricted(RoleEnum.ADMIN)
11
+ export class SyncNoteCreateInput extends SyncNoteInput {
12
+ @UnifiedField({ description: 'Title of the note', roles: RoleEnum.S_EVERYONE })
13
+ override title: string = undefined;
14
+ }
@@ -0,0 +1,23 @@
1
+ import { InputType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../../../core/common/decorators/restricted.decorator.js';
4
+ import { UnifiedField } from '../../../../core/common/decorators/unified-field.decorator.js';
5
+ import { RoleEnum } from '../../../../core/common/enums/role.enum.js';
6
+ import { CoreInput } from '../../../../core/common/inputs/core-input.input.js';
7
+
8
+ /**
9
+ * Input of a pushed change of a note — validated and whitelisted like a request body, so a client can set only what
10
+ * is declared here (never `createdBy`, `version` or `deletedAt`)
11
+ */
12
+ @InputType({ description: 'Input to change a synchronized note' })
13
+ @Restricted(RoleEnum.ADMIN)
14
+ export class SyncNoteInput extends CoreInput {
15
+ @UnifiedField({ description: 'Text of the note', isOptional: true, roles: RoleEnum.S_EVERYONE })
16
+ body?: string = undefined;
17
+
18
+ @UnifiedField({ description: 'Priority of the note', isOptional: true, roles: RoleEnum.S_EVERYONE })
19
+ priority?: number = undefined;
20
+
21
+ @UnifiedField({ description: 'Title of the note', isOptional: true, roles: RoleEnum.S_EVERYONE })
22
+ title?: string = undefined;
23
+ }
@@ -0,0 +1,77 @@
1
+ import { ObjectType } from '@nestjs/graphql';
2
+ import type { Types } from 'mongoose';
3
+
4
+ import { Restricted } from '../../../core/common/decorators/restricted.decorator.js';
5
+ import { UnifiedField } from '../../../core/common/decorators/unified-field.decorator.js';
6
+ import { RoleEnum } from '../../../core/common/enums/role.enum.js';
7
+ import { equalIds } from '../../../core/common/helpers/id.helper.js';
8
+ import { CorePersistenceModel } from '../../../core/common/models/core-persistence.model.js';
9
+ import {
10
+ objectIdType,
11
+ Schema as MongooseSchema,
12
+ SchemaFactory,
13
+ } from '../../../core/common/persistence/mongo/mongoose-compat.js';
14
+ import { PersistedModel } from '../../../core/common/persistence/persisted-model.decorator.js';
15
+ import { SyncField } from '../../../core/modules/sync/core-sync.decorators.js';
16
+
17
+ /**
18
+ * A note offered to offline clients — the reference implementation of a synchronized model
19
+ * (`src/core/modules/sync/INTEGRATION-CHECKLIST.md`).
20
+ *
21
+ * Not part of the reference server: `ServerModule` does not import it, so the framework's own suite runs without sync.
22
+ * The sync contract suite (`tests/sync-contract.e2e-spec.ts`) runs on exactly these files.
23
+ *
24
+ * - `sync.roles`: who may pull, create, change and delete through `/sync`. The roles of a project's own routes do not
25
+ * reach the sync routes, so they are declared here; `S_CREATOR` is decided against the stored record.
26
+ * - `sync.strategy: 'merge'`: a change based on an older version keeps the fields the client changed — unless one of
27
+ * them is `@SyncField('reject')`, which still conflicts.
28
+ * - `securityCheck()`: every user sees their own notes. A pull runs every record through it, a push decides with it
29
+ * whether an id belongs to a record the caller may see.
30
+ */
31
+ @PersistedModel({
32
+ sync: {
33
+ roles: {
34
+ create: [RoleEnum.S_USER],
35
+ delete: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
36
+ pull: [RoleEnum.S_USER],
37
+ update: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
38
+ },
39
+ strategy: 'merge',
40
+ },
41
+ })
42
+ @MongooseSchema({ timestamps: true })
43
+ @ObjectType({ description: 'Note synchronized to offline clients' })
44
+ @Restricted(RoleEnum.ADMIN)
45
+ export class SyncNote extends CorePersistenceModel {
46
+ @UnifiedField({ description: 'Text of the note', isOptional: true, mongoose: true, roles: RoleEnum.S_EVERYONE })
47
+ body?: string = undefined;
48
+
49
+ /** Set by `CrudService.create()`; what `S_CREATOR` and `securityCheck()` decide on */
50
+ @UnifiedField({
51
+ description: 'ID of the user who created the note',
52
+ isOptional: true,
53
+ mongoose: { type: objectIdType() },
54
+ roles: RoleEnum.S_EVERYONE,
55
+ swaggerApiOptions: { type: String },
56
+ type: () => String,
57
+ })
58
+ createdBy?: string | Types.ObjectId = undefined;
59
+
60
+ /** A concurrent change of the priority is never merged silently: it answers `conflict` */
61
+ @SyncField('reject')
62
+ @UnifiedField({ description: 'Priority of the note', isOptional: true, mongoose: true, roles: RoleEnum.S_EVERYONE })
63
+ priority?: number = undefined;
64
+
65
+ @UnifiedField({ description: 'Title of the note', isOptional: true, mongoose: true, roles: RoleEnum.S_EVERYONE })
66
+ title?: string = undefined;
67
+
68
+ /** Every user sees the notes they created; administrators see all */
69
+ override securityCheck(user: any, force?: boolean) {
70
+ if (force || user?.hasRole?.([RoleEnum.ADMIN]) || (user?.id && equalIds(this.createdBy, user))) {
71
+ return this;
72
+ }
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ export const SyncNoteSchema = SchemaFactory.createForClass(SyncNote);
@@ -0,0 +1,16 @@
1
+ import { Module } from '@nestjs/common';
2
+
3
+ import { CorePersistenceModule } from '../../../core/common/persistence/core-persistence.module.js';
4
+ import { SyncNote, SyncNoteSchema } from './sync-note.model.js';
5
+ import { SyncNoteService } from './sync-note.service.js';
6
+
7
+ /**
8
+ * The synchronized note model and its service. Registered with `CorePersistenceModule.forFeature()`, so the model
9
+ * follows `database` like every other — and only a model registered this way can be synchronized.
10
+ */
11
+ @Module({
12
+ exports: [SyncNoteService],
13
+ imports: [CorePersistenceModule.forFeature([{ model: SyncNote, name: SyncNote.name, schema: SyncNoteSchema }])],
14
+ providers: [SyncNoteService],
15
+ })
16
+ export class SyncNoteModule {}
@@ -0,0 +1,25 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ import type { DataStore } from '../../../core/common/persistence/data-store.interface.js';
4
+ import { InjectStore } from '../../../core/common/persistence/inject-store.decorator.js';
5
+ import { ConfigService } from '../../../core/common/services/config.service.js';
6
+ import { CrudService } from '../../../core/common/services/crud.service.js';
7
+ import { SyncService } from '../../../core/modules/sync/core-sync.decorators.js';
8
+ import { SyncNoteCreateInput } from './inputs/sync-note-create.input.js';
9
+ import { SyncNoteInput } from './inputs/sync-note.input.js';
10
+ import { SyncNote } from './sync-note.model.js';
11
+
12
+ /**
13
+ * The note service, offered to offline clients: `@SyncService()` puts the model under `/sync/SyncNote`, and every pull
14
+ * and push of it runs through this service — its `process()` pipeline, `securityCheck()`, the tenant policy.
15
+ */
16
+ @SyncService({ createInput: SyncNoteCreateInput, model: SyncNote, updateInput: SyncNoteInput })
17
+ @Injectable()
18
+ export class SyncNoteService extends CrudService<SyncNote, SyncNoteCreateInput, SyncNoteInput> {
19
+ constructor(
20
+ protected override readonly configService: ConfigService,
21
+ @InjectStore(SyncNote) store: DataStore<SyncNote>,
22
+ ) {
23
+ super({ configService, dataStore: store, mainModelConstructor: SyncNote });
24
+ }
25
+ }