@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.
- package/.claude/rules/architecture.md +1 -0
- package/.claude/rules/configurable-features.md +1 -0
- package/.claude/rules/package-management.md +16 -1
- package/.claude/rules/persistence.md +26 -0
- package/.claude/rules/testing.md +79 -14
- package/CLAUDE.md +2 -1
- package/FRAMEWORK-API.md +17 -2
- package/README.md +18 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +41 -0
- package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
- package/dist/core/common/persistence/core-persistence.module.js +12 -2
- package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
- package/dist/core/common/persistence/data-source.runtime.js +4 -2
- package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
- package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
- package/dist/core/common/persistence/id-format.d.ts +2 -0
- package/dist/core/common/persistence/id-format.js +14 -0
- package/dist/core/common/persistence/id-format.js.map +1 -1
- package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
- package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
- package/dist/core/common/persistence/model-metadata.registry.js +20 -2
- package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
- package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
- package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
- package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
- package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
- package/dist/core/common/persistence/mongoose-data-store.js +107 -31
- package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
- package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
- package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
- package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
- package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
- package/dist/core/common/persistence/policies/sync.policy.js +77 -0
- package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +101 -4
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
- package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
- package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
- package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
- package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
- package/dist/core/common/persistence/sql/sql-errors.js +3 -1
- package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
- package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
- package/dist/core/common/persistence/sync-access.registry.js +8 -0
- package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
- package/dist/core/common/persistence/sync-model.d.ts +29 -0
- package/dist/core/common/persistence/sync-model.js +138 -0
- package/dist/core/common/persistence/sync-model.js.map +1 -0
- package/dist/core/common/services/crud.service.d.ts +13 -1
- package/dist/core/common/services/crud.service.js +174 -16
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +54 -0
- package/dist/core/modules/error-code/error-codes.js +48 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
- package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
- package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
- package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
- package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
- package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
- package/dist/core/modules/sync/core-sync-cursor.js +77 -0
- package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
- package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
- package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
- package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
- package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
- package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
- package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
- package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
- package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
- package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
- package/dist/core/modules/sync/core-sync-state.store.js +64 -0
- package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
- package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
- package/dist/core/modules/sync/core-sync.constants.js +23 -0
- package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
- package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
- package/dist/core/modules/sync/core-sync.controller.js +79 -0
- package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
- package/dist/core/modules/sync/core-sync.decorators.js +28 -0
- package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
- package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
- package/dist/core/modules/sync/core-sync.module.js +50 -0
- package/dist/core/modules/sync/core-sync.module.js.map +1 -0
- package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
- package/dist/core/modules/sync/core-sync.resolver.js +46 -0
- package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
- package/dist/core/modules/sync/core-sync.service.d.ts +142 -0
- package/dist/core/modules/sync/core-sync.service.js +1208 -0
- package/dist/core/modules/sync/core-sync.service.js.map +1 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +4 -0
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core.module.js +14 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +6 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
- package/dist/server/modules/sync-note/sync-note.model.js +73 -0
- package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
- package/dist/server/modules/sync-note/sync-note.module.js +21 -0
- package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
- package/dist/server/modules/sync-note/sync-note.service.js +35 -0
- package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/docs/offline-sync-protocol.md +96 -62
- package/docs/security-overrides.md +14 -0
- package/migration-guides/12.1.x-to-12.2.x.md +154 -0
- package/package.json +4 -2
- package/src/core/common/interfaces/server-options.interface.ts +149 -0
- package/src/core/common/interfaces/service-options.interface.ts +15 -0
- package/src/core/common/persistence/core-persistence.module.ts +21 -2
- package/src/core/common/persistence/data-source.runtime.ts +5 -2
- package/src/core/common/persistence/data-store.interface.ts +65 -12
- package/src/core/common/persistence/id-format.ts +26 -0
- package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
- package/src/core/common/persistence/model-metadata.registry.ts +31 -2
- package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
- package/src/core/common/persistence/mongoose-data-store.ts +171 -35
- package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
- package/src/core/common/persistence/policies/sync.policy.ts +121 -0
- package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
- package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
- package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +129 -3
- package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
- package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
- package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
- package/src/core/common/persistence/sql/sql-errors.ts +5 -1
- package/src/core/common/persistence/sync-access.registry.ts +50 -0
- package/src/core/common/persistence/sync-model.ts +270 -0
- package/src/core/common/services/crud.service.ts +257 -19
- package/src/core/modules/error-code/error-codes.ts +59 -0
- package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
- package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
- package/src/core/modules/sync/README.md +188 -0
- package/src/core/modules/sync/SECURITY.md +65 -0
- package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
- package/src/core/modules/sync/core-sync-cursor.ts +117 -0
- package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
- package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
- package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
- package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
- package/src/core/modules/sync/core-sync-state.store.ts +89 -0
- package/src/core/modules/sync/core-sync.constants.ts +51 -0
- package/src/core/modules/sync/core-sync.controller.ts +68 -0
- package/src/core/modules/sync/core-sync.decorators.ts +84 -0
- package/src/core/modules/sync/core-sync.module.ts +75 -0
- package/src/core/modules/sync/core-sync.resolver.ts +35 -0
- package/src/core/modules/sync/core-sync.service.ts +1728 -0
- package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
- package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +8 -0
- package/src/core.module.ts +25 -0
- package/src/index.ts +21 -0
- package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
- package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
- package/src/server/modules/sync-note/sync-note.model.ts +77 -0
- package/src/server/modules/sync-note/sync-note.module.ts +16 -0
- 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
|
/**
|
package/src/core.module.ts
CHANGED
|
@@ -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
|
+
}
|