@pattern-stack/codegen 0.4.1 → 0.4.3
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/CHANGELOG.md +6 -0
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -0
- package/dist/runtime/subsystems/bridge/bridge.module.js +38 -21
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +1 -0
- package/dist/runtime/subsystems/bridge/index.js +29 -12
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +31 -14
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -0
- package/dist/runtime/subsystems/jobs/index.js +27 -10
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +9 -4
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.js +6 -2
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +27 -10
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -4
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/src/cli/index.js +29 -2
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +2 -1
- package/runtime/analytics/index.ts +31 -0
- package/runtime/analytics/metrics.ts +85 -0
- package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
- package/runtime/analytics/packs/index.ts +5 -0
- package/runtime/analytics/packs/monetary-measures.ts +20 -0
- package/runtime/analytics/specs.ts +54 -0
- package/runtime/analytics/types.ts +105 -0
- package/runtime/base-classes/activity-entity-repository.ts +50 -0
- package/runtime/base-classes/activity-entity-service.ts +48 -0
- package/runtime/base-classes/base-read-use-cases.ts +88 -0
- package/runtime/base-classes/base-repository.ts +289 -0
- package/runtime/base-classes/base-service.ts +183 -0
- package/runtime/base-classes/index.ts +38 -0
- package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
- package/runtime/base-classes/knowledge-entity-service.ts +14 -0
- package/runtime/base-classes/lifecycle-events.ts +152 -0
- package/runtime/base-classes/metadata-entity-repository.ts +80 -0
- package/runtime/base-classes/metadata-entity-service.ts +48 -0
- package/runtime/base-classes/synced-entity-repository.ts +57 -0
- package/runtime/base-classes/synced-entity-service.ts +50 -0
- package/runtime/base-classes/with-analytics.ts +22 -0
- package/runtime/constants/tokens.ts +29 -0
- package/runtime/eav-helpers.ts +74 -0
- package/runtime/pipes/zod-validation.pipe.ts +64 -0
- package/runtime/shared/openapi/error-response.dto.ts +24 -0
- package/runtime/shared/openapi/errors.ts +39 -0
- package/runtime/shared/openapi/index.ts +20 -0
- package/runtime/shared/openapi/registry.tokens.ts +13 -0
- package/runtime/shared/openapi/registry.ts +151 -0
- package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
- package/runtime/subsystems/analytics/analytics.module.ts +64 -0
- package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
- package/runtime/subsystems/analytics/cube-backend.ts +75 -0
- package/runtime/subsystems/analytics/index.ts +15 -0
- package/runtime/subsystems/analytics/noop-backend.ts +27 -0
- package/runtime/subsystems/auth/auth.module.ts +91 -0
- package/runtime/subsystems/auth/auth.tokens.ts +27 -0
- package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
- package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
- package/runtime/subsystems/auth/index.ts +77 -0
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
- package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
- package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
- package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
- package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
- package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
- package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
- package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
- package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
- package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
- package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
- package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
- package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
- package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
- package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
- package/runtime/subsystems/bridge/bridge.module.ts +160 -0
- package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
- package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
- package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
- package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
- package/runtime/subsystems/bridge/generated/registry.ts +6 -0
- package/runtime/subsystems/bridge/index.ts +84 -0
- package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
- package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
- package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
- package/runtime/subsystems/cache/cache.module.ts +115 -0
- package/runtime/subsystems/cache/cache.protocol.ts +45 -0
- package/runtime/subsystems/cache/cache.schema.ts +27 -0
- package/runtime/subsystems/cache/cache.tokens.ts +17 -0
- package/runtime/subsystems/cache/index.ts +22 -0
- package/runtime/subsystems/events/domain-events.schema.ts +77 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
- package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
- package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
- package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
- package/runtime/subsystems/events/events-errors.ts +30 -0
- package/runtime/subsystems/events/events.module.ts +230 -0
- package/runtime/subsystems/events/events.tokens.ts +62 -0
- package/runtime/subsystems/events/generated/bus.ts +103 -0
- package/runtime/subsystems/events/generated/index.ts +7 -0
- package/runtime/subsystems/events/generated/registry.ts +84 -0
- package/runtime/subsystems/events/generated/schemas.ts +59 -0
- package/runtime/subsystems/events/generated/types.ts +94 -0
- package/runtime/subsystems/events/index.ts +21 -0
- package/runtime/subsystems/index.ts +63 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
- package/runtime/subsystems/jobs/index.ts +120 -0
- package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
- package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
- package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
- package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +860 -0
- package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
- package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
- package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
- package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +312 -0
- package/runtime/subsystems/jobs/job-worker.ts +624 -0
- package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
- package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
- package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
- package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
- package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
- package/runtime/subsystems/storage/index.ts +18 -0
- package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
- package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
- package/runtime/subsystems/storage/storage.module.ts +60 -0
- package/runtime/subsystems/storage/storage.protocol.ts +78 -0
- package/runtime/subsystems/storage/storage.tokens.ts +9 -0
- package/runtime/subsystems/storage/storage.utils.ts +20 -0
- package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
- package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
- package/runtime/subsystems/sync/index.ts +98 -0
- package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
- package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
- package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
- package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
- package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
- package/runtime/subsystems/sync/sync-errors.ts +54 -0
- package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
- package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
- package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
- package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
- package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
- package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
- package/runtime/subsystems/sync/sync.module.ts +156 -0
- package/runtime/subsystems/sync/sync.tokens.ts +57 -0
- package/runtime/types/drizzle.ts +23 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync subsystem — field-diff protocol (port)
|
|
3
|
+
*
|
|
4
|
+
* `IFieldDiffer<T>` is the pluggable differ seam. The default implementation
|
|
5
|
+
* (`DeepEqualDiffer`, ships in SYNC-5) walks every field except an ignore
|
|
6
|
+
* list; CDC-aware differs can skip comparison for fields the provider didn't
|
|
7
|
+
* flag as changed.
|
|
8
|
+
*
|
|
9
|
+
* `FieldDiffSchema` is the structural enforcement of the `changed_fields`
|
|
10
|
+
* column per ADR-0003 — enforced at write time by the recorder service so
|
|
11
|
+
* consumers can rely on the shape in downstream queries.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// FieldDiff shape — the ADR-0003 contract
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Structured per-field change. Enforced shape for `sync_run_items.changed_fields`.
|
|
21
|
+
*
|
|
22
|
+
* `created` items set `from: null, to: <value>` for every non-null field.
|
|
23
|
+
* `deleted` items set `from: <value>, to: null`.
|
|
24
|
+
* `noop` items carry `{}`.
|
|
25
|
+
*/
|
|
26
|
+
export const FieldDiffValueSchema = z.object({
|
|
27
|
+
from: z.unknown(),
|
|
28
|
+
to: z.unknown(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const FieldDiffSchema = z.record(z.string(), FieldDiffValueSchema);
|
|
32
|
+
|
|
33
|
+
export type FieldDiffValue = z.infer<typeof FieldDiffValueSchema>;
|
|
34
|
+
export type FieldDiff = z.infer<typeof FieldDiffSchema>;
|
|
35
|
+
|
|
36
|
+
/** Result of comparing a new record against its existing local state. */
|
|
37
|
+
export type DiffResult = FieldDiff | 'noop';
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// IFieldDiffer
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pluggable differ. Default ships in SYNC-5 as `DeepEqualDiffer<T>` —
|
|
45
|
+
* deep-equal over every field except an ignore list (`updated_at` and other
|
|
46
|
+
* row metadata). CDC-aware differs restrict comparison to
|
|
47
|
+
* `providerChangedFields` when supplied.
|
|
48
|
+
*/
|
|
49
|
+
export interface IFieldDiffer<T> {
|
|
50
|
+
/**
|
|
51
|
+
* @param existing — current local state, or `null` when the record is new
|
|
52
|
+
* @param incoming — the canonical record coming from the adapter
|
|
53
|
+
* @param providerChangedFields — optional hint from CDC-capable sources;
|
|
54
|
+
* when present, differ may restrict the comparison to these fields
|
|
55
|
+
*/
|
|
56
|
+
diff(
|
|
57
|
+
existing: T | null,
|
|
58
|
+
incoming: T,
|
|
59
|
+
providerChangedFields?: string[],
|
|
60
|
+
): DiffResult;
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync subsystem — loopback-fingerprint protocol (port)
|
|
3
|
+
*
|
|
4
|
+
* Optional port. When the local system writes to an upstream provider via an
|
|
5
|
+
* outbound path, the same change typically echoes back on the next inbound
|
|
6
|
+
* poll/CDC/webhook. A fingerprint store lets `ExecuteSyncUseCase` skip
|
|
7
|
+
* records it already wrote, avoiding a diff-noop round trip and a spurious
|
|
8
|
+
* audit row.
|
|
9
|
+
*
|
|
10
|
+
* The contract is deliberately narrow: one method, one decision
|
|
11
|
+
* ("is this change an echo of our own recent write?"). Provider-specific
|
|
12
|
+
* fingerprinting (hash a canonical payload, TTL shorter than the poll
|
|
13
|
+
* interval, etc.) lives in the concrete backend. The subsystem does not ship
|
|
14
|
+
* a backend in Phase 1; consumers that need loopback suppression provide
|
|
15
|
+
* their own (redis-hashed, memory TTL, etc.).
|
|
16
|
+
*
|
|
17
|
+
* `entityType` is `string` (not a union) — per dealbrain-v2's HS-9 findings
|
|
18
|
+
* the CRM-specific narrowing `'opportunity' | 'account' | 'contact'` bled
|
|
19
|
+
* into the port and had to be removed. Consumers narrow internally if they
|
|
20
|
+
* want.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface ILoopbackFingerprintStore<T = unknown> {
|
|
24
|
+
/**
|
|
25
|
+
* @returns `true` when the record matches a recent local write (skip it)
|
|
26
|
+
* `false` when the record is external-originated (process it)
|
|
27
|
+
*/
|
|
28
|
+
isEchoOfOwnWrite(
|
|
29
|
+
entityType: string,
|
|
30
|
+
externalId: string,
|
|
31
|
+
record: T,
|
|
32
|
+
): Promise<boolean>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrizzleSyncRunRecorder — Drizzle-backed `ISyncRunRecorder` (SYNC-4).
|
|
3
|
+
*
|
|
4
|
+
* Generic write path only — extracted from dealbrain-v2's
|
|
5
|
+
* `SyncRunRecorderService`, minus CRM-specific convenience methods. Those
|
|
6
|
+
* stay consumer-owned; the subsystem ships the substrate.
|
|
7
|
+
*
|
|
8
|
+
* ## Responsibilities
|
|
9
|
+
*
|
|
10
|
+
* - `startRun` — INSERT sync_runs row in status='running', returns id.
|
|
11
|
+
* - `recordItem` — validates `changedFields` via `FieldDiffSchema.parse`
|
|
12
|
+
* BEFORE the INSERT; a malformed shape throws before
|
|
13
|
+
* any DB call fires. Enforces the ADR-0003 contract at
|
|
14
|
+
* the write boundary.
|
|
15
|
+
* - `completeRun` — UPDATE sync_runs with terminal status, counts,
|
|
16
|
+
* cursor_after, duration_ms, completed_at.
|
|
17
|
+
*
|
|
18
|
+
* ## Multi-tenancy
|
|
19
|
+
*
|
|
20
|
+
* When `SYNC_MULTI_TENANT` is true (SYNC-6):
|
|
21
|
+
* - `startRun` and `recordItem` require non-null `tenantId` on input.
|
|
22
|
+
* Enforcement goes through the shared `assertTenantId` helper so the
|
|
23
|
+
* error message shape matches the orchestrator entry point + the
|
|
24
|
+
* cursor-store backends.
|
|
25
|
+
* - `completeRun` does NOT re-check tenancy — the run id was returned
|
|
26
|
+
* by `startRun` which already enforced it, and run ids are uuids that
|
|
27
|
+
* aren't guessable cross-tenant. Matches JOB-3's pattern of trusting
|
|
28
|
+
* the run-id for downstream mutations.
|
|
29
|
+
*/
|
|
30
|
+
import { Inject, Injectable, Optional } from '@nestjs/common';
|
|
31
|
+
import { eq } from 'drizzle-orm';
|
|
32
|
+
import type { DrizzleClient } from '../../types/drizzle';
|
|
33
|
+
import { DRIZZLE } from '../../constants/tokens';
|
|
34
|
+
import type {
|
|
35
|
+
CompleteRunInput,
|
|
36
|
+
ISyncRunRecorder,
|
|
37
|
+
RecordItemInput,
|
|
38
|
+
StartRunInput,
|
|
39
|
+
} from './sync-run-recorder.protocol';
|
|
40
|
+
import { syncRuns, syncRunItems } from './sync-audit.schema';
|
|
41
|
+
import { FieldDiffSchema } from './sync-field-diff.protocol';
|
|
42
|
+
import { SYNC_MULTI_TENANT } from './sync.tokens';
|
|
43
|
+
import { assertTenantId } from './sync-errors';
|
|
44
|
+
|
|
45
|
+
@Injectable()
|
|
46
|
+
export class DrizzleSyncRunRecorder implements ISyncRunRecorder {
|
|
47
|
+
private readonly multiTenant: boolean;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
@Inject(DRIZZLE) private readonly db: DrizzleClient,
|
|
51
|
+
@Optional() @Inject(SYNC_MULTI_TENANT) multiTenant?: boolean,
|
|
52
|
+
) {
|
|
53
|
+
this.multiTenant = multiTenant ?? false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async startRun(input: StartRunInput): Promise<{ id: string }> {
|
|
57
|
+
assertTenantId(input.tenantId, {
|
|
58
|
+
multiTenant: this.multiTenant,
|
|
59
|
+
operation: 'startRun',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const rows = await this.db
|
|
63
|
+
.insert(syncRuns)
|
|
64
|
+
.values({
|
|
65
|
+
subscriptionId: input.subscriptionId,
|
|
66
|
+
direction: input.direction,
|
|
67
|
+
action: input.action,
|
|
68
|
+
status: 'running',
|
|
69
|
+
cursorBefore: input.cursorBefore ?? null,
|
|
70
|
+
tenantId: input.tenantId ?? null,
|
|
71
|
+
})
|
|
72
|
+
.returning({ id: syncRuns.id });
|
|
73
|
+
|
|
74
|
+
const id = rows[0]?.id;
|
|
75
|
+
if (!id) {
|
|
76
|
+
// Drizzle's insert().returning() contract: at least one row is
|
|
77
|
+
// returned for every successful INSERT. A missing id would indicate
|
|
78
|
+
// a driver misbehavior; throw loudly rather than return bogus data.
|
|
79
|
+
throw new Error('DrizzleSyncRunRecorder: INSERT RETURNING produced no id');
|
|
80
|
+
}
|
|
81
|
+
return { id };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async recordItem(input: RecordItemInput): Promise<void> {
|
|
85
|
+
assertTenantId(input.tenantId, {
|
|
86
|
+
multiTenant: this.multiTenant,
|
|
87
|
+
operation: 'recordItem',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ADR-0003 contract enforcement — reject malformed changedFields
|
|
91
|
+
// before the DB call fires. `parse` throws a ZodError; callers see
|
|
92
|
+
// the validation failure, not a DB constraint error.
|
|
93
|
+
FieldDiffSchema.parse(input.changedFields);
|
|
94
|
+
|
|
95
|
+
await this.db.insert(syncRunItems).values({
|
|
96
|
+
syncRunId: input.syncRunId,
|
|
97
|
+
entityType: input.entityType,
|
|
98
|
+
externalId: input.externalId,
|
|
99
|
+
localId: input.localId ?? null,
|
|
100
|
+
operation: input.operation,
|
|
101
|
+
status: input.status,
|
|
102
|
+
changedFields: input.changedFields,
|
|
103
|
+
title: input.title ?? null,
|
|
104
|
+
error: input.error ?? null,
|
|
105
|
+
tenantId: input.tenantId ?? null,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async completeRun(runId: string, input: CompleteRunInput): Promise<void> {
|
|
110
|
+
await this.db
|
|
111
|
+
.update(syncRuns)
|
|
112
|
+
.set({
|
|
113
|
+
status: input.status,
|
|
114
|
+
recordsFound: input.recordsFound,
|
|
115
|
+
recordsProcessed: input.recordsProcessed,
|
|
116
|
+
cursorAfter: input.cursorAfter ?? null,
|
|
117
|
+
durationMs: input.durationMs,
|
|
118
|
+
error: input.error ?? null,
|
|
119
|
+
completedAt: new Date(),
|
|
120
|
+
})
|
|
121
|
+
.where(eq(syncRuns.id, runId));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryRunRecorder — in-memory backend for `ISyncRunRecorder` (SYNC-6).
|
|
3
|
+
*
|
|
4
|
+
* Test double so `SyncModule.forRoot({ backend: 'memory' })` is genuinely
|
|
5
|
+
* end-to-end runnable without Postgres. Mirrors the role of
|
|
6
|
+
* `MemoryCursorStore`: plain keyed state, `clear()` helper for
|
|
7
|
+
* `beforeEach` resets, public inspection surface so tests can assert on
|
|
8
|
+
* the recorded run + item timeline without scraping logs.
|
|
9
|
+
*
|
|
10
|
+
* Validates `changedFields` through `FieldDiffSchema.parse` on every
|
|
11
|
+
* `recordItem` call — same ADR-0003 contract as the Drizzle backend. An
|
|
12
|
+
* in-memory recorder that skipped the validation would be a silently
|
|
13
|
+
* weaker contract than production.
|
|
14
|
+
*
|
|
15
|
+
* `startRun` generates a uuid via `crypto.randomUUID()` (Node 19+ / Bun).
|
|
16
|
+
* We don't import `uuid` because the subsystem has no other use for it.
|
|
17
|
+
*
|
|
18
|
+
* ## Multi-tenancy
|
|
19
|
+
*
|
|
20
|
+
* `tenantId` is accepted (and recorded on the in-memory row so tests can
|
|
21
|
+
* assert it) but enforcement lives at the module boundary. The memory
|
|
22
|
+
* backend intentionally does not throw on missing `tenantId` — that's
|
|
23
|
+
* the orchestrator's job when `multiTenant=true` (SYNC-6). A permissive
|
|
24
|
+
* memory recorder lets tests exercise error paths where the orchestrator
|
|
25
|
+
* short-circuits before ever reaching the recorder.
|
|
26
|
+
*/
|
|
27
|
+
import { Injectable } from '@nestjs/common';
|
|
28
|
+
import type {
|
|
29
|
+
CompleteRunInput,
|
|
30
|
+
ISyncRunRecorder,
|
|
31
|
+
RecordItemInput,
|
|
32
|
+
StartRunInput,
|
|
33
|
+
} from './sync-run-recorder.protocol';
|
|
34
|
+
import { FieldDiffSchema } from './sync-field-diff.protocol';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Concrete run row as held in memory. Shape mirrors the interesting
|
|
38
|
+
* columns on `sync_runs` so assertions read like DB queries.
|
|
39
|
+
*/
|
|
40
|
+
export interface MemoryRunRecord {
|
|
41
|
+
id: string;
|
|
42
|
+
subscriptionId: string;
|
|
43
|
+
direction: 'inbound' | 'outbound';
|
|
44
|
+
action: 'poll' | 'cdc' | 'webhook' | 'manual' | 'writeback';
|
|
45
|
+
status: 'running' | 'success' | 'no_changes' | 'failed';
|
|
46
|
+
cursorBefore: unknown | null;
|
|
47
|
+
cursorAfter: unknown | null;
|
|
48
|
+
recordsFound: number;
|
|
49
|
+
recordsProcessed: number;
|
|
50
|
+
durationMs: number | null;
|
|
51
|
+
error: string | null;
|
|
52
|
+
tenantId: string | null;
|
|
53
|
+
startedAt: Date;
|
|
54
|
+
completedAt: Date | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Injectable()
|
|
58
|
+
export class MemoryRunRecorder implements ISyncRunRecorder {
|
|
59
|
+
/**
|
|
60
|
+
* All started runs keyed by id. Public so tests can inspect lifecycle
|
|
61
|
+
* transitions without poking through recording methods.
|
|
62
|
+
*/
|
|
63
|
+
readonly runs: Map<string, MemoryRunRecord> = new Map();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Items keyed by `sync_run_id`, array order matches insertion order —
|
|
67
|
+
* mirrors the timeline the `(sync_run_id, created_at)` index produces
|
|
68
|
+
* in Postgres.
|
|
69
|
+
*/
|
|
70
|
+
readonly items: Map<string, RecordItemInput[]> = new Map();
|
|
71
|
+
|
|
72
|
+
async startRun(input: StartRunInput): Promise<{ id: string }> {
|
|
73
|
+
const id = crypto.randomUUID();
|
|
74
|
+
this.runs.set(id, {
|
|
75
|
+
id,
|
|
76
|
+
subscriptionId: input.subscriptionId,
|
|
77
|
+
direction: input.direction,
|
|
78
|
+
action: input.action,
|
|
79
|
+
status: 'running',
|
|
80
|
+
cursorBefore: input.cursorBefore ?? null,
|
|
81
|
+
cursorAfter: null,
|
|
82
|
+
recordsFound: 0,
|
|
83
|
+
recordsProcessed: 0,
|
|
84
|
+
durationMs: null,
|
|
85
|
+
error: null,
|
|
86
|
+
tenantId: input.tenantId ?? null,
|
|
87
|
+
startedAt: new Date(),
|
|
88
|
+
completedAt: null,
|
|
89
|
+
});
|
|
90
|
+
this.items.set(id, []);
|
|
91
|
+
return { id };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async recordItem(input: RecordItemInput): Promise<void> {
|
|
95
|
+
// Same ADR-0003 contract as the Drizzle backend.
|
|
96
|
+
FieldDiffSchema.parse(input.changedFields);
|
|
97
|
+
|
|
98
|
+
const bucket = this.items.get(input.syncRunId);
|
|
99
|
+
if (!bucket) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`MemoryRunRecorder.recordItem: no run started for id '${input.syncRunId}'. ` +
|
|
102
|
+
`Call startRun(...) first.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
bucket.push(input);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async completeRun(runId: string, input: CompleteRunInput): Promise<void> {
|
|
109
|
+
const run = this.runs.get(runId);
|
|
110
|
+
if (!run) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`MemoryRunRecorder.completeRun: no run started for id '${runId}'.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
run.status = input.status;
|
|
116
|
+
run.recordsFound = input.recordsFound;
|
|
117
|
+
run.recordsProcessed = input.recordsProcessed;
|
|
118
|
+
run.cursorAfter = input.cursorAfter ?? null;
|
|
119
|
+
run.durationMs = input.durationMs;
|
|
120
|
+
run.error = input.error ?? null;
|
|
121
|
+
run.completedAt = new Date();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Reset state. Tests call this in `beforeEach`. */
|
|
125
|
+
clear(): void {
|
|
126
|
+
this.runs.clear();
|
|
127
|
+
this.items.clear();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── test ergonomics ─────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/** All runs for a subscription, newest first. Timeline reads. */
|
|
133
|
+
getRunsForSubscription(subscriptionId: string): MemoryRunRecord[] {
|
|
134
|
+
return Array.from(this.runs.values())
|
|
135
|
+
.filter((r) => r.subscriptionId === subscriptionId)
|
|
136
|
+
.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** All item rows for a run, insertion-ordered. */
|
|
140
|
+
getItemsForRun(runId: string): RecordItemInput[] {
|
|
141
|
+
return this.items.get(runId) ?? [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync subsystem — run-recorder protocol (port)
|
|
3
|
+
*
|
|
4
|
+
* `ISyncRunRecorder` is the write side of the audit log. `ExecuteSyncUseCase`
|
|
5
|
+
* (SYNC-5) calls `startRun` at the top of the loop, `recordItem` for each
|
|
6
|
+
* processed change, and `completeRun` in a `finally` block so a run always
|
|
7
|
+
* reaches a terminal status.
|
|
8
|
+
*
|
|
9
|
+
* The Drizzle backend (SYNC-4) persists against `sync_runs` / `sync_run_items`
|
|
10
|
+
* from the SYNC-1 schema. Tests use lightweight in-memory fakes — no
|
|
11
|
+
* dedicated memory backend ships; the surface is small enough that inline
|
|
12
|
+
* fakes keep the intent local to each spec.
|
|
13
|
+
*
|
|
14
|
+
* `changed_fields` on `recordItem` is validated by the implementation via
|
|
15
|
+
* `FieldDiffSchema.parse` (ADR-0003 contract). Orchestrator callers pass the
|
|
16
|
+
* `DiffResult` the differ returned — `'noop'` is translated to `{}` by the
|
|
17
|
+
* orchestrator before reaching the recorder, so the recorder's input is
|
|
18
|
+
* always a `FieldDiff`.
|
|
19
|
+
*/
|
|
20
|
+
import type { FieldDiff } from './sync-field-diff.protocol';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Lifecycle — inputs
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/** Args for `startRun`. Mirrors the non-nullable columns on `sync_runs`. */
|
|
27
|
+
export interface StartRunInput {
|
|
28
|
+
readonly subscriptionId: string;
|
|
29
|
+
readonly direction: 'inbound' | 'outbound';
|
|
30
|
+
readonly action: 'poll' | 'cdc' | 'webhook' | 'manual' | 'writeback';
|
|
31
|
+
/** Cursor snapshot at run start, or `null` if this is the first run. */
|
|
32
|
+
readonly cursorBefore: unknown | null;
|
|
33
|
+
/**
|
|
34
|
+
* Tenant id when `SYNC_MULTI_TENANT` is enabled. The recorder's own
|
|
35
|
+
* boundary rule (SYNC-6) enforces non-null when the flag is on;
|
|
36
|
+
* orchestrator passes it through from `ExecuteSyncInput.tenantId`.
|
|
37
|
+
*/
|
|
38
|
+
readonly tenantId?: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Args for `recordItem`. Mirrors the non-nullable columns on `sync_run_items`. */
|
|
42
|
+
export interface RecordItemInput {
|
|
43
|
+
readonly syncRunId: string;
|
|
44
|
+
readonly entityType: string;
|
|
45
|
+
readonly externalId: string;
|
|
46
|
+
readonly localId?: string | null;
|
|
47
|
+
readonly operation: 'created' | 'updated' | 'deleted' | 'noop';
|
|
48
|
+
readonly status: 'success' | 'failed' | 'skipped';
|
|
49
|
+
/**
|
|
50
|
+
* Structured per-field diff — ADR-0003. `{}` for noop / skipped items.
|
|
51
|
+
* The recorder validates this against `FieldDiffSchema` on every write.
|
|
52
|
+
*/
|
|
53
|
+
readonly changedFields: FieldDiff;
|
|
54
|
+
readonly title?: string | null;
|
|
55
|
+
readonly error?: string | null;
|
|
56
|
+
readonly tenantId?: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Args for `completeRun`. */
|
|
60
|
+
export interface CompleteRunInput {
|
|
61
|
+
readonly status: 'success' | 'no_changes' | 'failed';
|
|
62
|
+
readonly recordsFound: number;
|
|
63
|
+
readonly recordsProcessed: number;
|
|
64
|
+
readonly cursorAfter: unknown | null;
|
|
65
|
+
readonly durationMs: number;
|
|
66
|
+
readonly error?: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// ISyncRunRecorder
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
export interface ISyncRunRecorder {
|
|
74
|
+
/** Opens a new `sync_runs` row in `status = 'running'`. Returns the run id. */
|
|
75
|
+
startRun(input: StartRunInput): Promise<{ id: string }>;
|
|
76
|
+
|
|
77
|
+
/** Appends one `sync_run_items` row. Throws if `changedFields` is malformed. */
|
|
78
|
+
recordItem(input: RecordItemInput): Promise<void>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Finalizes the run. Must be called from a `finally` block so an in-flight
|
|
82
|
+
* run never gets stuck in `'running'` state; the orchestrator passes
|
|
83
|
+
* `'failed'` when the iteration body threw.
|
|
84
|
+
*/
|
|
85
|
+
completeRun(runId: string, input: CompleteRunInput): Promise<void>;
|
|
86
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync subsystem — sync-sink protocol (port)
|
|
3
|
+
*
|
|
4
|
+
* Write surface for the generic orchestrator. One per canonical entity type.
|
|
5
|
+
*
|
|
6
|
+
* **Shape contract:** the sink speaks the *canonical* `TCanonical` externally
|
|
7
|
+
* — `findByExternalId` returns a canonical-shaped view of local state
|
|
8
|
+
* (columns projected to canonical field names) so the differ compares
|
|
9
|
+
* like-for-like against `Change.record` from the adapter. Internal DB
|
|
10
|
+
* mapping (canonical → local write, EAV dual-write, FK resolution) stays
|
|
11
|
+
* inside the sink implementation.
|
|
12
|
+
*
|
|
13
|
+
* Implementations compose the entity's service + (when the entity has EAV)
|
|
14
|
+
* `FieldValueService` inside a single transaction. ADR-13-revised.
|
|
15
|
+
*/
|
|
16
|
+
export interface ISyncSink<TCanonical> {
|
|
17
|
+
/**
|
|
18
|
+
* Canonical-shaped view of local state, or `null` when no local row exists.
|
|
19
|
+
* Called once per change to source the diff's "before" side.
|
|
20
|
+
*/
|
|
21
|
+
findByExternalId(
|
|
22
|
+
userId: string,
|
|
23
|
+
externalId: string,
|
|
24
|
+
): Promise<TCanonical | null>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Insert-or-update by `external_id`. Must:
|
|
28
|
+
* - run EAV dual-write in a single transaction when the entity has `fields`
|
|
29
|
+
* - resolve FK references (e.g. `account_id` from `accountExternalId`)
|
|
30
|
+
* via a repository lookup
|
|
31
|
+
* - stamp `user_id` and `provider` from caller / context
|
|
32
|
+
* - tolerate re-entry (same record twice in a window = no-op)
|
|
33
|
+
*
|
|
34
|
+
* Returns the local row id and the canonical projection of the saved row
|
|
35
|
+
* (so the orchestrator can record it on `sync_run_items.local_id`).
|
|
36
|
+
*
|
|
37
|
+
* `provider` is the adapter domain string (e.g. `'salesforce-crm'`,
|
|
38
|
+
* `'hubspot-crm'`) persisted on the DB row. Passed from
|
|
39
|
+
* `ExecuteSyncInput.provider`.
|
|
40
|
+
*/
|
|
41
|
+
upsertByExternalId(
|
|
42
|
+
userId: string,
|
|
43
|
+
record: TCanonical,
|
|
44
|
+
provider: string,
|
|
45
|
+
): Promise<{ id: string; saved: TCanonical }>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Soft-delete by `external_id`. Called when `Change.operation === 'deleted'`.
|
|
49
|
+
* Returns `null` when no local row exists (orchestrator records a no-op).
|
|
50
|
+
*/
|
|
51
|
+
softDeleteByExternalId(
|
|
52
|
+
userId: string,
|
|
53
|
+
externalId: string,
|
|
54
|
+
): Promise<{ id: string } | null>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncModule — `DynamicModule.forRoot({ backend, multiTenant? })` factory
|
|
3
|
+
* wiring the sync subsystem's substrate (SYNC-6, ADR-008 subsystem pattern).
|
|
4
|
+
*
|
|
5
|
+
* ## What this module provides
|
|
6
|
+
*
|
|
7
|
+
* - `SYNC_CURSOR_STORE` — Drizzle or Memory cursor store
|
|
8
|
+
* - `SYNC_RUN_RECORDER` — Drizzle or Memory run recorder
|
|
9
|
+
* - `SYNC_FIELD_DIFFER` — default `DeepEqualDiffer`
|
|
10
|
+
* - `SYNC_MULTI_TENANT` — resolved boolean flag (defaults to false)
|
|
11
|
+
* - `SYNC_MODULE_OPTIONS` — the options object itself, for backends
|
|
12
|
+
* that need to inspect config at construction time
|
|
13
|
+
*
|
|
14
|
+
* ## What this module does NOT provide
|
|
15
|
+
*
|
|
16
|
+
* - `SYNC_CHANGE_SOURCE` — per-provider per-entity; consumer binds in
|
|
17
|
+
* their feature module (e.g. `OpportunitySyncModule` provides a
|
|
18
|
+
* `SalesforceOpportunityChangeSource`).
|
|
19
|
+
* - `SYNC_SINK` — per canonical entity; consumer binds in their feature
|
|
20
|
+
* module.
|
|
21
|
+
* - `SYNC_LOOPBACK_FINGERPRINT_STORE` — optional; consumer provides
|
|
22
|
+
* only when they have outbound writeback paths. `@Optional()` at the
|
|
23
|
+
* orchestrator seam means absence is fine.
|
|
24
|
+
* - `ExecuteSyncUseCase` — registered by the feature module alongside
|
|
25
|
+
* its source + sink bindings. Providing the orchestrator here would
|
|
26
|
+
* force Nest to resolve SYNC_CHANGE_SOURCE + SYNC_SINK at module
|
|
27
|
+
* compile time, which fails when the feature module hasn't been
|
|
28
|
+
* imported yet. Consumers register `ExecuteSyncUseCase` in the same
|
|
29
|
+
* `providers` array as their source + sink so resolution is local
|
|
30
|
+
* to where all three are bound.
|
|
31
|
+
*
|
|
32
|
+
* Same shape as `EventsModule.forRoot` — the module wires the bus; you
|
|
33
|
+
* bring your own handlers. Here: the module wires the substrate; you
|
|
34
|
+
* bring your own source + sink.
|
|
35
|
+
*
|
|
36
|
+
* ## Usage
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* // AppModule — single source of truth for backend + multi-tenancy.
|
|
40
|
+
* @Module({
|
|
41
|
+
* imports: [SyncModule.forRoot({ backend: 'drizzle' })],
|
|
42
|
+
* })
|
|
43
|
+
* export class AppModule {}
|
|
44
|
+
*
|
|
45
|
+
* // Per-entity feature module — binds source + sink, gets the
|
|
46
|
+
* // orchestrator for free.
|
|
47
|
+
* @Module({
|
|
48
|
+
* providers: [
|
|
49
|
+
* { provide: SYNC_CHANGE_SOURCE, useClass: SalesforceOpportunitySource },
|
|
50
|
+
* { provide: SYNC_SINK, useClass: OpportunitySyncSink },
|
|
51
|
+
* ExecuteSyncUseCase,
|
|
52
|
+
* ],
|
|
53
|
+
* })
|
|
54
|
+
* export class OpportunitySyncModule {
|
|
55
|
+
* constructor(
|
|
56
|
+
* private readonly execute: ExecuteSyncUseCase<CanonicalOpportunity>,
|
|
57
|
+
* ) {}
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* `global: true` means feature modules do not need to re-import
|
|
62
|
+
* `SyncModule` — the substrate tokens are available project-wide.
|
|
63
|
+
*/
|
|
64
|
+
import { Module, type DynamicModule, type Provider } from '@nestjs/common';
|
|
65
|
+
import {
|
|
66
|
+
SYNC_CURSOR_STORE,
|
|
67
|
+
SYNC_FIELD_DIFFER,
|
|
68
|
+
SYNC_MODULE_OPTIONS,
|
|
69
|
+
SYNC_MULTI_TENANT,
|
|
70
|
+
SYNC_RUN_RECORDER,
|
|
71
|
+
} from './sync.tokens';
|
|
72
|
+
import { MemoryCursorStore } from './sync-cursor-store.memory-backend';
|
|
73
|
+
import { MemoryRunRecorder } from './sync-run-recorder.memory-backend';
|
|
74
|
+
import { PostgresCursorStore } from './sync-cursor-store.drizzle-backend';
|
|
75
|
+
import { DrizzleSyncRunRecorder } from './sync-run-recorder.drizzle-backend';
|
|
76
|
+
import { DeepEqualDiffer } from './deep-equal.differ';
|
|
77
|
+
|
|
78
|
+
export interface SyncModuleOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Backend selection. `drizzle` wires the Postgres cursor store +
|
|
81
|
+
* run-log recorder; `memory` wires in-memory doubles suitable for
|
|
82
|
+
* tests + local dev.
|
|
83
|
+
*/
|
|
84
|
+
backend: 'drizzle' | 'memory';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Multi-tenancy opt-in (SYNC-6).
|
|
88
|
+
*
|
|
89
|
+
* When `true`, every call to the orchestrator + both Drizzle backends
|
|
90
|
+
* must supply a non-null `tenantId`; missing values throw
|
|
91
|
+
* `MissingTenantIdError`. Defense-in-depth: the orchestrator rejects
|
|
92
|
+
* at entry (no dangling `status=running` rows) AND the Drizzle
|
|
93
|
+
* backends reject at their write boundary (belt-and-braces for any
|
|
94
|
+
* path that bypasses the orchestrator). Both sites use the shared
|
|
95
|
+
* `assertTenantId` helper so error messages match.
|
|
96
|
+
*
|
|
97
|
+
* Memory backends accept `tenantId` unconditionally — their state is
|
|
98
|
+
* process-local; cross-tenant isolation there is not meaningful.
|
|
99
|
+
*
|
|
100
|
+
* Defaults to `false`.
|
|
101
|
+
*/
|
|
102
|
+
multiTenant?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Module({})
|
|
106
|
+
export class SyncModule {
|
|
107
|
+
static forRoot(options: SyncModuleOptions): DynamicModule {
|
|
108
|
+
const multiTenant = options.multiTenant ?? false;
|
|
109
|
+
|
|
110
|
+
const sharedProviders: Provider[] = [
|
|
111
|
+
{ provide: SYNC_MODULE_OPTIONS, useValue: options },
|
|
112
|
+
{ provide: SYNC_MULTI_TENANT, useValue: multiTenant },
|
|
113
|
+
// Default differ — consumers can override by binding a different
|
|
114
|
+
// `IFieldDiffer<T>` to `SYNC_FIELD_DIFFER` in their feature module.
|
|
115
|
+
{ provide: SYNC_FIELD_DIFFER, useValue: new DeepEqualDiffer() },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const backendProviders: Provider[] =
|
|
119
|
+
options.backend === 'memory'
|
|
120
|
+
? [
|
|
121
|
+
// Wired as singletons via `useValue` so tests can pull
|
|
122
|
+
// them out via `moduleRef.get(MemoryCursorStore)` for
|
|
123
|
+
// direct assertions. Matches JOB-4 / MemoryJobStore shape.
|
|
124
|
+
{ provide: MemoryCursorStore, useValue: new MemoryCursorStore() },
|
|
125
|
+
{
|
|
126
|
+
provide: SYNC_CURSOR_STORE,
|
|
127
|
+
useExisting: MemoryCursorStore,
|
|
128
|
+
},
|
|
129
|
+
{ provide: MemoryRunRecorder, useValue: new MemoryRunRecorder() },
|
|
130
|
+
{
|
|
131
|
+
provide: SYNC_RUN_RECORDER,
|
|
132
|
+
useExisting: MemoryRunRecorder,
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
: [
|
|
136
|
+
// Drizzle backends — injected with DRIZZLE (provided by the
|
|
137
|
+
// consumer's DrizzleModule) + the SYNC_MULTI_TENANT flag
|
|
138
|
+
// we bound above.
|
|
139
|
+
{ provide: SYNC_CURSOR_STORE, useClass: PostgresCursorStore },
|
|
140
|
+
{ provide: SYNC_RUN_RECORDER, useClass: DrizzleSyncRunRecorder },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
module: SyncModule,
|
|
145
|
+
global: true,
|
|
146
|
+
providers: [...sharedProviders, ...backendProviders],
|
|
147
|
+
exports: [
|
|
148
|
+
SYNC_MODULE_OPTIONS,
|
|
149
|
+
SYNC_MULTI_TENANT,
|
|
150
|
+
SYNC_FIELD_DIFFER,
|
|
151
|
+
SYNC_CURSOR_STORE,
|
|
152
|
+
SYNC_RUN_RECORDER,
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|