@open-mercato/core 0.6.8-develop.7026.1.23445b3f3e → 0.6.8-develop.7029.1.a1bb3363af
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/dist/modules/data_sync/lib/sync-engine.js +72 -6
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/data_sync/AGENTS.md +1 -1
- package/src/modules/data_sync/lib/adapter.ts +36 -0
- package/src/modules/data_sync/lib/sync-engine.ts +113 -5
- package/src/modules/sync_excel/lib/adapters/customers.ts +5 -0
|
@@ -50,6 +50,9 @@ function applyExportCounters(batch) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
const HEARTBEAT_TICK_MS = STALE_JOB_TIMEOUT_SECONDS * 1e3 / 4;
|
|
53
|
+
function isAbortError(error) {
|
|
54
|
+
return typeof error === "object" && error !== null && error.name === "AbortError";
|
|
55
|
+
}
|
|
53
56
|
async function* withHeartbeat(source, tick, intervalMs) {
|
|
54
57
|
const iterator = source[Symbol.asyncIterator]();
|
|
55
58
|
try {
|
|
@@ -122,6 +125,25 @@ function createSyncEngine(deps) {
|
|
|
122
125
|
});
|
|
123
126
|
};
|
|
124
127
|
}
|
|
128
|
+
function makeCancellationTick(progressJobId, scope, controller) {
|
|
129
|
+
if (!progressJobId) return () => {
|
|
130
|
+
};
|
|
131
|
+
let inFlight = false;
|
|
132
|
+
return () => {
|
|
133
|
+
if (inFlight || controller.signal.aborted) return;
|
|
134
|
+
inFlight = true;
|
|
135
|
+
progressService.isCancellationRequested(progressJobId, scope.tenantId, scope.organizationId).then((cancelled) => {
|
|
136
|
+
if (cancelled) controller.abort();
|
|
137
|
+
}).catch((error) => {
|
|
138
|
+
logger.warn("Cancellation poll failed", {
|
|
139
|
+
progressJobId,
|
|
140
|
+
error: error instanceof Error ? error.message : String(error)
|
|
141
|
+
});
|
|
142
|
+
}).finally(() => {
|
|
143
|
+
inFlight = false;
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|
|
125
147
|
async function refreshCoverageSnapshots(entityTypes, scope) {
|
|
126
148
|
if (!entityTypes || entityTypes.length === 0) return;
|
|
127
149
|
await Promise.allSettled(
|
|
@@ -424,8 +446,12 @@ function createSyncEngine(deps) {
|
|
|
424
446
|
let processedCount = await seedProcessedCount(run.progressJobId, scope);
|
|
425
447
|
let totalCount = null;
|
|
426
448
|
let committedBatches = activeRun.batchesCompleted ?? 0;
|
|
449
|
+
let streamReportedDone = false;
|
|
427
450
|
const runTrace = captureTelemetryTrace();
|
|
428
451
|
const spanAttributes = runSpanAttributes(run, providerKey, scope);
|
|
452
|
+
const cancellation = new AbortController();
|
|
453
|
+
const heartbeat = makeHeartbeatTick(run.progressJobId, scope);
|
|
454
|
+
const pollCancellation = makeCancellationTick(run.progressJobId, scope, cancellation);
|
|
429
455
|
try {
|
|
430
456
|
const streamResult = await forEachBatch(
|
|
431
457
|
withHeartbeat(
|
|
@@ -437,9 +463,18 @@ function createSyncEngine(deps) {
|
|
|
437
463
|
mapping,
|
|
438
464
|
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
439
465
|
runId: run.id,
|
|
440
|
-
parameters: run.parameters ?? {}
|
|
466
|
+
parameters: run.parameters ?? {},
|
|
467
|
+
signal: cancellation.signal
|
|
441
468
|
}),
|
|
442
|
-
|
|
469
|
+
// `finally` so a synchronous throw from the heartbeat cannot also stop cancellation
|
|
470
|
+
// from being observed for the rest of the run.
|
|
471
|
+
() => {
|
|
472
|
+
try {
|
|
473
|
+
heartbeat();
|
|
474
|
+
} finally {
|
|
475
|
+
pollCancellation();
|
|
476
|
+
}
|
|
477
|
+
},
|
|
443
478
|
HEARTBEAT_TICK_MS
|
|
444
479
|
),
|
|
445
480
|
{
|
|
@@ -453,7 +488,7 @@ function createSyncEngine(deps) {
|
|
|
453
488
|
"data_sync.batch_index": batch.batchIndex,
|
|
454
489
|
"data_sync.batch_size": batch.items.length
|
|
455
490
|
});
|
|
456
|
-
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
491
|
+
if (cancellation.signal.aborted || run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
457
492
|
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
458
493
|
return "stop";
|
|
459
494
|
}
|
|
@@ -479,6 +514,7 @@ function createSyncEngine(deps) {
|
|
|
479
514
|
{ expectedBatchesCompleted: committedBatches, persistSharedCursor }
|
|
480
515
|
);
|
|
481
516
|
committedBatches += 1;
|
|
517
|
+
streamReportedDone = batch.hasMore === false;
|
|
482
518
|
await updateProgress(run.progressJobId, processedCount, totalCount, scope);
|
|
483
519
|
await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope);
|
|
484
520
|
await logImportItemFailures(run.id, run.integrationId, batch.items, scope);
|
|
@@ -510,6 +546,10 @@ function createSyncEngine(deps) {
|
|
|
510
546
|
});
|
|
511
547
|
return;
|
|
512
548
|
}
|
|
549
|
+
if (cancellation.signal.aborted && isAbortError(error)) {
|
|
550
|
+
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
513
553
|
const message = error instanceof Error ? error.message : "Sync import failed";
|
|
514
554
|
await integrationLogService.write(
|
|
515
555
|
{
|
|
@@ -523,6 +563,10 @@ function createSyncEngine(deps) {
|
|
|
523
563
|
await finalizeRun(run.id, "failed", scope, message, operationalTelemetry);
|
|
524
564
|
return;
|
|
525
565
|
}
|
|
566
|
+
if (cancellation.signal.aborted && !streamReportedDone) {
|
|
567
|
+
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
526
570
|
await finalizeRun(run.id, "completed", scope, void 0, operationalTelemetry);
|
|
527
571
|
},
|
|
528
572
|
async runExport(runId, batchSize, scope) {
|
|
@@ -596,8 +640,12 @@ function createSyncEngine(deps) {
|
|
|
596
640
|
const mapping = await resolveMapping(adapter, run.entityType, scope);
|
|
597
641
|
let processedCount = await seedProcessedCount(run.progressJobId, scope);
|
|
598
642
|
let committedBatches = activeRun.batchesCompleted ?? 0;
|
|
643
|
+
let streamReportedDone = false;
|
|
599
644
|
const runTrace = captureTelemetryTrace();
|
|
600
645
|
const spanAttributes = runSpanAttributes(run, providerKey, scope);
|
|
646
|
+
const cancellation = new AbortController();
|
|
647
|
+
const heartbeat = makeHeartbeatTick(run.progressJobId, scope);
|
|
648
|
+
const pollCancellation = makeCancellationTick(run.progressJobId, scope, cancellation);
|
|
601
649
|
try {
|
|
602
650
|
const streamResult = await forEachBatch(
|
|
603
651
|
withHeartbeat(
|
|
@@ -609,9 +657,18 @@ function createSyncEngine(deps) {
|
|
|
609
657
|
mapping,
|
|
610
658
|
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
611
659
|
runId: run.id,
|
|
612
|
-
parameters: run.parameters ?? {}
|
|
660
|
+
parameters: run.parameters ?? {},
|
|
661
|
+
signal: cancellation.signal
|
|
613
662
|
}),
|
|
614
|
-
|
|
663
|
+
// `finally` so a synchronous throw from the heartbeat cannot also stop cancellation
|
|
664
|
+
// from being observed for the rest of the run.
|
|
665
|
+
() => {
|
|
666
|
+
try {
|
|
667
|
+
heartbeat();
|
|
668
|
+
} finally {
|
|
669
|
+
pollCancellation();
|
|
670
|
+
}
|
|
671
|
+
},
|
|
615
672
|
HEARTBEAT_TICK_MS
|
|
616
673
|
),
|
|
617
674
|
{
|
|
@@ -625,7 +682,7 @@ function createSyncEngine(deps) {
|
|
|
625
682
|
"data_sync.batch_index": batch.batchIndex,
|
|
626
683
|
"data_sync.batch_size": batch.results.length
|
|
627
684
|
});
|
|
628
|
-
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
685
|
+
if (cancellation.signal.aborted || run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
629
686
|
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
630
687
|
return "stop";
|
|
631
688
|
}
|
|
@@ -651,6 +708,7 @@ function createSyncEngine(deps) {
|
|
|
651
708
|
{ expectedBatchesCompleted: committedBatches, persistSharedCursor }
|
|
652
709
|
);
|
|
653
710
|
committedBatches += 1;
|
|
711
|
+
streamReportedDone = batch.hasMore === false;
|
|
654
712
|
await updateProgress(run.progressJobId, processedCount, null, scope);
|
|
655
713
|
await logExportItemFailures(run.id, run.integrationId, batch.results, scope);
|
|
656
714
|
await writeOperationalLog({
|
|
@@ -680,6 +738,10 @@ function createSyncEngine(deps) {
|
|
|
680
738
|
});
|
|
681
739
|
return;
|
|
682
740
|
}
|
|
741
|
+
if (cancellation.signal.aborted && isAbortError(error)) {
|
|
742
|
+
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
683
745
|
const message = error instanceof Error ? error.message : "Sync export failed";
|
|
684
746
|
await integrationLogService.write(
|
|
685
747
|
{
|
|
@@ -693,6 +755,10 @@ function createSyncEngine(deps) {
|
|
|
693
755
|
await finalizeRun(run.id, "failed", scope, message, operationalTelemetry);
|
|
694
756
|
return;
|
|
695
757
|
}
|
|
758
|
+
if (cancellation.signal.aborted && !streamReportedDone) {
|
|
759
|
+
await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
696
762
|
await finalizeRun(run.id, "completed", scope, void 0, operationalTelemetry);
|
|
697
763
|
}
|
|
698
764
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/lib/sync-engine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport type { IntegrationLogService } from '../../integrations/lib/log-service'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport { STALE_JOB_TIMEOUT_SECONDS } from '../../progress/lib/progressService'\nimport { refreshCoverageSnapshot } from '../../query_index/lib/coverage'\nimport { emitDataSyncEvent } from '../events'\nimport type { DataSyncAdapter, DataMapping, ExportBatch, ImportBatch, RunParameterValue } from './adapter'\nimport { getDataSyncAdapter, resolveProviderKey } from './adapter-registry'\nimport type { SyncRunService } from './sync-run-service'\nimport { SyncRunOwnershipConflictError } from './sync-run-service'\nimport { forEachBatch } from './batch-stream'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n captureTelemetryTrace,\n type TelemetrySpanAttributes,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { SyncRun } from '../data/entities'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-engine' })\n\ntype RunParameters = Record<string, RunParameterValue>\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\ntype EngineDeps = {\n em: EntityManager\n syncRunService: SyncRunService\n integrationCredentialsService: CredentialsService\n integrationLogService: IntegrationLogService\n integrationStateService?: IntegrationStateService\n progressService: ProgressService\n}\n\n/** Repeated on every batch span so a rooted batch trace identifies its run on its own. */\nfunction runSpanAttributes(run: SyncRun, providerKey: string, scope: SyncScope): TelemetrySpanAttributes {\n return {\n 'data_sync.run_id': run.id,\n 'data_sync.integration_id': run.integrationId,\n 'data_sync.provider_key': providerKey,\n 'data_sync.entity_type': run.entityType,\n 'data_sync.direction': run.direction,\n 'om.tenant_id': scope.tenantId,\n 'om.organization_id': scope.organizationId,\n }\n}\n\nfunction applyImportCounters(batch: ImportBatch): Pick<Required<SyncCounterDelta>, 'createdCount' | 'updatedCount' | 'skippedCount' | 'failedCount'> {\n let createdCount = 0\n let updatedCount = 0\n let skippedCount = 0\n let failedCount = 0\n\n for (const item of batch.items) {\n if (item.action === 'create') createdCount += 1\n else if (item.action === 'update') updatedCount += 1\n else if (item.action === 'failed') failedCount += 1\n else skippedCount += 1\n }\n\n return { createdCount, updatedCount, skippedCount, failedCount }\n}\n\ntype SyncCounterDelta = {\n createdCount?: number\n updatedCount?: number\n skippedCount?: number\n failedCount?: number\n processedCount: number\n}\n\nfunction applyExportCounters(batch: ExportBatch): SyncCounterDelta {\n let failedCount = 0\n let skippedCount = 0\n let updatedCount = 0\n\n for (const result of batch.results) {\n if (result.status === 'error') failedCount += 1\n else if (result.status === 'skipped') skippedCount += 1\n else updatedCount += 1\n }\n\n return {\n failedCount,\n skippedCount,\n updatedCount,\n processedCount: batch.results.length,\n }\n}\n\n// Adapter batches can legitimately outlast the stale-job sweep window (slow upstream\n// APIs), so the engine must heartbeat while a batch is still being produced.\nconst HEARTBEAT_TICK_MS = (STALE_JOB_TIMEOUT_SECONDS * 1000) / 4\n\n// Runs `tick` on an interval only while the source iterator is pending, so heartbeats\n// stop the moment the producer dies and genuinely stale jobs still get swept. The outer\n// finally closes the adapter generator on early exits (cancellation, ownership conflict).\nasync function* withHeartbeat<T>(source: AsyncIterable<T>, tick: () => void, intervalMs: number): AsyncGenerator<T, void, undefined> {\n const iterator = source[Symbol.asyncIterator]()\n try {\n while (true) {\n const timer = setInterval(tick, intervalMs)\n let result: IteratorResult<T>\n try {\n result = await iterator.next()\n } finally {\n clearInterval(timer)\n }\n if (result.done) return\n yield result.value\n }\n } finally {\n await iterator.return?.()\n }\n}\n\nexport function createSyncEngine(deps: EngineDeps) {\n const { syncRunService, integrationCredentialsService, integrationLogService, integrationStateService, progressService } = deps\n\n async function resolveMapping(adapter: DataSyncAdapter, entityType: string, scope: SyncScope): Promise<DataMapping> {\n return adapter.getMapping({\n entityType,\n scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },\n })\n }\n\n async function updateProgress(progressJobId: string | null | undefined, processedCount: number, totalCount: number | null, scope: SyncScope): Promise<void> {\n if (!progressJobId) return\n\n await progressService.updateProgress(\n progressJobId,\n {\n processedCount,\n totalCount: totalCount ?? undefined,\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n }\n\n // On redelivery the progress counter must resume where the last delivery left off the\n // same way committedBatches does \u2014 updateProgress writes absolute counts, so starting at\n // zero would regress the visible count. The progress job's own processedCount is the only\n // persisted value already in the engine's unit: `batch.processedCount ?? items.length`,\n // i.e. source records. The run's created/updated/skipped/failed counters count emitted\n // items, which adapters may explode several-per-source-record (Akeneo yields a product\n // plus its variants), so seeding from them would overshoot the total and pin the bar.\n async function seedProcessedCount(progressJobId: string | null | undefined, scope: SyncScope): Promise<number> {\n if (!progressJobId) return 0\n const job = await progressService.getJob(progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n return job?.processedCount ?? 0\n }\n\n function makeHeartbeatTick(progressJobId: string | null | undefined, scope: SyncScope): () => void {\n const touchJobHeartbeat = progressService.touchJobHeartbeat?.bind(progressService)\n if (!progressJobId || !touchJobHeartbeat) return () => {}\n let inFlight = false\n return () => {\n if (inFlight) return\n inFlight = true\n touchJobHeartbeat(progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n .catch((error) => {\n logger.warn('Progress heartbeat failed', {\n progressJobId,\n error: error instanceof Error ? error.message : String(error),\n })\n })\n .finally(() => {\n inFlight = false\n })\n }\n }\n\n async function refreshCoverageSnapshots(entityTypes: string[] | undefined, scope: SyncScope): Promise<void> {\n if (!entityTypes || entityTypes.length === 0) return\n\n await Promise.allSettled(\n Array.from(new Set(entityTypes.filter((value) => typeof value === 'string' && value.trim().length > 0)))\n .map((entityType) => refreshCoverageSnapshot(deps.em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })),\n )\n }\n\n async function logImportItemFailures(\n runId: string,\n integrationId: string,\n items: ImportBatch['items'],\n scope: SyncScope,\n ): Promise<void> {\n const failedItems = items.filter((item) => item.action === 'failed')\n for (const item of failedItems) {\n const errorMessage = typeof item.data.errorMessage === 'string' && item.data.errorMessage.trim().length > 0\n ? item.data.errorMessage.trim()\n : 'Import item failed'\n const sourceProductUuid = typeof item.data.sourceProductUuid === 'string' && item.data.sourceProductUuid.trim().length > 0\n ? item.data.sourceProductUuid.trim()\n : null\n const sourceIdentifier = typeof item.data.sourceIdentifier === 'string' && item.data.sourceIdentifier.trim().length > 0\n ? item.data.sourceIdentifier.trim()\n : null\n const message = [\n `Failed to import item ${item.externalId}`,\n sourceProductUuid ? `(uuid: ${sourceProductUuid})` : null,\n sourceIdentifier ? `(identifier: ${sourceIdentifier})` : null,\n `: ${errorMessage}`,\n ].filter((part) => part !== null).join(' ')\n\n await integrationLogService.write(\n {\n integrationId,\n runId,\n level: 'error',\n message,\n payload: item.data,\n },\n scope,\n )\n }\n }\n\n async function logExportItemFailures(\n runId: string,\n integrationId: string,\n results: ExportBatch['results'],\n scope: SyncScope,\n ): Promise<void> {\n const failedResults = results.filter((result) => result.status === 'error' && result.error)\n for (const result of failedResults) {\n const label = result.externalId ? `${result.externalId} (id: ${result.localId})` : result.localId\n const errorMessage = result.error!.split('\\n')[0]\n const message = `Failed to export item ${label}: ${errorMessage}`\n\n await integrationLogService.write(\n {\n integrationId,\n runId,\n level: 'error',\n message,\n payload: { kind: 'export-item-failure', summary: result.error },\n },\n scope,\n )\n }\n }\n\n async function writeOperationalLog(params: {\n integrationId: string\n runId: string\n level: 'info' | 'warn' | 'error'\n message: string\n scope: SyncScope\n enabled: boolean\n payload?: Record<string, unknown>\n }): Promise<void> {\n if (!params.enabled) return\n\n await integrationLogService.write(\n {\n integrationId: params.integrationId,\n runId: params.runId,\n level: params.level,\n message: params.message,\n payload: params.payload,\n },\n params.scope,\n )\n }\n\n async function updateOperationalState(params: {\n integrationId: string\n status: 'healthy' | 'degraded' | 'unhealthy'\n scope: SyncScope\n enabled: boolean\n }): Promise<void> {\n if (!params.enabled || !integrationStateService) return\n\n await integrationStateService.upsert(\n params.integrationId,\n {\n lastHealthStatus: params.status,\n lastHealthCheckedAt: new Date(),\n },\n params.scope,\n )\n }\n\n async function finalizeRun(\n runId: string,\n status: 'completed' | 'failed' | 'cancelled',\n scope: SyncScope,\n error?: string,\n operationalTelemetry = false,\n ): Promise<void> {\n const existingRun = await syncRunService.getRun(runId, scope)\n const alreadyFinalizedWithSameStatus = existingRun?.status === status\n && (status === 'completed' || status === 'failed' || status === 'cancelled')\n\n const run = await syncRunService.markStatus(runId, status, scope, error)\n if (!run) return\n\n if (alreadyFinalizedWithSameStatus) {\n return\n }\n\n if (run.status !== status) {\n // `markStatus` refuses a terminal -> different-terminal transition and\n // returns the row unchanged, so the run is already finished under another\n // delivery of this job. Everything below \u2014 the progress job, the\n // operational log and the lifecycle event \u2014 would describe the wrong\n // outcome, and `data_sync.run.failed` is dispatched to tenant webhooks.\n // A displaced worker stays silent instead.\n logger.warn('Skipping finalization of a sync run another worker already finalized', {\n runId,\n requestedStatus: status,\n actualStatus: run.status,\n })\n return\n }\n\n if (run.progressJobId) {\n if (status === 'completed') {\n await progressService.completeJob(\n run.progressJobId,\n {\n resultSummary: {\n createdCount: run.createdCount,\n updatedCount: run.updatedCount,\n skippedCount: run.skippedCount,\n failedCount: run.failedCount,\n batchesCompleted: run.batchesCompleted,\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n } else if (status === 'failed') {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: error ?? 'Sync run failed',\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n } else if (status === 'cancelled') {\n await progressService.markCancelled(\n run.progressJobId,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n }\n }\n\n if (status === 'completed') {\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'healthy',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: 'Sync run completed',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'completed',\n summary: `Sync completed with ${run.createdCount} created, ${run.updatedCount} updated, ${run.failedCount} failed.`,\n createdCount: run.createdCount,\n updatedCount: run.updatedCount,\n skippedCount: run.skippedCount,\n failedCount: run.failedCount,\n batchesCompleted: run.batchesCompleted,\n },\n })\n } else if (status === 'cancelled') {\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'degraded',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'warn',\n message: 'Sync run cancelled',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'cancelled',\n summary: 'The sync run was cancelled before completion.',\n },\n })\n } else {\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'unhealthy',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'error',\n message: error ?? 'Sync run failed',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'failed',\n summary: error ?? 'The sync run failed.',\n },\n })\n }\n\n if (status === 'completed') {\n await emitDataSyncEvent('data_sync.run.completed', {\n runId,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n return\n }\n\n if (status === 'cancelled') {\n await emitDataSyncEvent('data_sync.run.cancelled', {\n runId,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n return\n }\n\n await emitDataSyncEvent('data_sync.run.failed', {\n runId,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n error: error ?? null,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n }\n\n return {\n async runImport(runId: string, batchSize: number, scope: SyncScope): Promise<void> {\n const run = await syncRunService.getRun(runId, scope)\n if (!run) {\n logger.warn('Skipping stale import job for missing run', { runId })\n return\n }\n if (run.status === 'cancelled') {\n if (run.progressJobId) {\n await progressService.markCancelled(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n return\n }\n\n const providerKey = resolveProviderKey(run.integrationId)\n const adapter = getDataSyncAdapter(providerKey)\n if (!adapter?.streamImport) {\n throw new Error(`No import adapter registered for provider ${providerKey}`)\n }\n const operationalTelemetry = adapter.operationalTelemetry === true\n const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true\n\n const credentials = await integrationCredentialsService.resolve(run.integrationId, scope)\n if (!credentials) {\n throw new Error(`Integration ${run.integrationId} is missing credentials`)\n }\n\n // A run already `running` means a stalled job was redelivered, so this is\n // a resume rather than a first start. Consumers see one `started` event per\n // delivery either way; the flag is what lets them tell the two apart.\n const resumed = run.status === 'running'\n const activeRun = await syncRunService.markStatus(run.id, 'running', scope)\n if (!activeRun || activeRun.status !== 'running') {\n return\n }\n await emitDataSyncEvent('data_sync.run.started', {\n runId: run.id,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n resumed,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'degraded',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: 'Sync run started',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Import run started for ${run.entityType}.`,\n entityType: run.entityType,\n direction: run.direction,\n },\n })\n\n if (run.progressJobId) {\n await progressService.startJob(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n\n const mapping = await resolveMapping(adapter, run.entityType, scope)\n let processedCount = await seedProcessedCount(run.progressJobId, scope)\n let totalCount: number | null = null\n let committedBatches = activeRun.batchesCompleted ?? 0\n // Captured while the triggering job's span is still the active one, so\n // every rooted batch trace can link back to it.\n const runTrace = captureTelemetryTrace()\n const spanAttributes = runSpanAttributes(run, providerKey, scope)\n\n try {\n const streamResult = await forEachBatch(\n withHeartbeat(\n adapter.streamImport({\n entityType: run.entityType,\n cursor: run.cursor ?? undefined,\n batchSize,\n credentials,\n mapping,\n scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },\n runId: run.id,\n parameters: (run.parameters ?? {}) as RunParameters,\n }),\n makeHeartbeatTick(run.progressJobId, scope),\n HEARTBEAT_TICK_MS,\n ),\n {\n spanName: 'data_sync.import.batch',\n drainSpanName: 'data_sync.import.drain',\n attributes: spanAttributes,\n linkTo: runTrace,\n },\n async (batch, span) => {\n span.setAttributes({\n 'data_sync.batch_index': batch.batchIndex,\n 'data_sync.batch_size': batch.items.length,\n })\n\n if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return 'stop'\n }\n\n const delta = applyImportCounters(batch)\n const processedBatchCount = batch.processedCount ?? batch.items.length\n processedCount += processedBatchCount\n totalCount = batch.totalEstimate ?? totalCount\n\n span.setAttributes({\n 'data_sync.processed_count': processedBatchCount,\n 'data_sync.created_count': delta.createdCount,\n 'data_sync.updated_count': delta.updatedCount,\n 'data_sync.skipped_count': delta.skippedCount,\n 'data_sync.failed_count': delta.failedCount,\n })\n\n await syncRunService.commitBatchProgress(\n run.id,\n {\n ...delta,\n batchesCompleted: 1,\n },\n batch.cursor,\n scope,\n { expectedBatchesCompleted: committedBatches, persistSharedCursor },\n )\n committedBatches += 1\n\n await updateProgress(run.progressJobId, processedCount, totalCount, scope)\n await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope)\n await logImportItemFailures(run.id, run.integrationId, batch.items, scope)\n\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: batch.message?.trim().length\n ? batch.message.trim()\n : `Processed import batch ${batch.batchIndex}`,\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Processed ${processedCount}${totalCount ? ` of ${totalCount}` : ''} rows so far.`,\n processedCount,\n batchSize: batch.items.length,\n processedBatchCount,\n cursor: batch.cursor,\n },\n })\n\n return 'continue'\n },\n )\n if (streamResult === 'stopped') return\n } catch (error) {\n if (error instanceof SyncRunOwnershipConflictError) {\n logger.warn('Yielding import run to a concurrent worker that already advanced it', {\n runId: run.id,\n expectedBatchesCompleted: error.expectedBatchesCompleted,\n })\n return\n }\n const message = error instanceof Error ? error.message : 'Sync import failed'\n await integrationLogService.write(\n {\n integrationId: run.integrationId,\n runId: run.id,\n level: 'error',\n message,\n },\n scope,\n )\n await finalizeRun(run.id, 'failed', scope, message, operationalTelemetry)\n return\n }\n\n await finalizeRun(run.id, 'completed', scope, undefined, operationalTelemetry)\n },\n\n async runExport(runId: string, batchSize: number, scope: SyncScope): Promise<void> {\n const run = await syncRunService.getRun(runId, scope)\n if (!run) {\n logger.warn('Skipping stale export job for missing run', { runId })\n return\n }\n if (run.status === 'cancelled') {\n if (run.progressJobId) {\n await progressService.markCancelled(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n return\n }\n\n const providerKey = resolveProviderKey(run.integrationId)\n const adapter = getDataSyncAdapter(providerKey)\n if (!adapter?.streamExport) {\n throw new Error(`No export adapter registered for provider ${providerKey}`)\n }\n const operationalTelemetry = adapter.operationalTelemetry === true\n const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true\n\n const credentials = await integrationCredentialsService.resolve(run.integrationId, scope)\n if (!credentials) {\n throw new Error(`Integration ${run.integrationId} is missing credentials`)\n }\n\n // A run already `running` means a stalled job was redelivered, so this is\n // a resume rather than a first start. Consumers see one `started` event per\n // delivery either way; the flag is what lets them tell the two apart.\n const resumed = run.status === 'running'\n const activeRun = await syncRunService.markStatus(run.id, 'running', scope)\n if (!activeRun || activeRun.status !== 'running') {\n return\n }\n await emitDataSyncEvent('data_sync.run.started', {\n runId: run.id,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n resumed,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'degraded',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: 'Sync run started',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Export run started for ${run.entityType}.`,\n entityType: run.entityType,\n direction: run.direction,\n },\n })\n\n if (run.progressJobId) {\n await progressService.startJob(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n\n const mapping = await resolveMapping(adapter, run.entityType, scope)\n let processedCount = await seedProcessedCount(run.progressJobId, scope)\n let committedBatches = activeRun.batchesCompleted ?? 0\n // Captured while the triggering job's span is still the active one, so\n // every rooted batch trace can link back to it.\n const runTrace = captureTelemetryTrace()\n const spanAttributes = runSpanAttributes(run, providerKey, scope)\n\n try {\n const streamResult = await forEachBatch(\n withHeartbeat(\n adapter.streamExport({\n entityType: run.entityType,\n cursor: run.cursor ?? undefined,\n batchSize,\n credentials,\n mapping,\n scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },\n runId: run.id,\n parameters: (run.parameters ?? {}) as RunParameters,\n }),\n makeHeartbeatTick(run.progressJobId, scope),\n HEARTBEAT_TICK_MS,\n ),\n {\n spanName: 'data_sync.export.batch',\n drainSpanName: 'data_sync.export.drain',\n attributes: spanAttributes,\n linkTo: runTrace,\n },\n async (batch, span) => {\n span.setAttributes({\n 'data_sync.batch_index': batch.batchIndex,\n 'data_sync.batch_size': batch.results.length,\n })\n\n if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return 'stop'\n }\n\n const delta = applyExportCounters(batch)\n processedCount += delta.processedCount\n\n span.setAttributes({\n 'data_sync.processed_count': delta.processedCount,\n 'data_sync.updated_count': delta.updatedCount,\n 'data_sync.skipped_count': delta.skippedCount,\n 'data_sync.failed_count': delta.failedCount,\n })\n\n await syncRunService.commitBatchProgress(\n run.id,\n {\n createdCount: 0,\n updatedCount: delta.updatedCount,\n skippedCount: delta.skippedCount,\n failedCount: delta.failedCount,\n batchesCompleted: 1,\n },\n batch.cursor,\n scope,\n { expectedBatchesCompleted: committedBatches, persistSharedCursor },\n )\n committedBatches += 1\n await updateProgress(run.progressJobId, processedCount, null, scope)\n await logExportItemFailures(run.id, run.integrationId, batch.results, scope)\n\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: `Processed export batch ${batch.batchIndex}`,\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Processed ${processedCount} export items so far.`,\n processedCount,\n batchSize: batch.results.length,\n cursor: batch.cursor,\n },\n })\n\n return 'continue'\n },\n )\n if (streamResult === 'stopped') return\n } catch (error) {\n if (error instanceof SyncRunOwnershipConflictError) {\n logger.warn('Yielding export run to a concurrent worker that already advanced it', {\n runId: run.id,\n expectedBatchesCompleted: error.expectedBatchesCompleted,\n })\n return\n }\n const message = error instanceof Error ? error.message : 'Sync export failed'\n await integrationLogService.write(\n {\n integrationId: run.integrationId,\n runId: run.id,\n level: 'error',\n message,\n },\n scope,\n )\n await finalizeRun(run.id, 'failed', scope, message, operationalTelemetry)\n return\n }\n\n await finalizeRun(run.id, 'completed', scope, undefined, operationalTelemetry)\n },\n }\n}\n\nexport type SyncEngine = ReturnType<typeof createSyncEngine>\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport type { IntegrationLogService } from '../../integrations/lib/log-service'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport { STALE_JOB_TIMEOUT_SECONDS } from '../../progress/lib/progressService'\nimport { refreshCoverageSnapshot } from '../../query_index/lib/coverage'\nimport { emitDataSyncEvent } from '../events'\nimport type { DataSyncAdapter, DataMapping, ExportBatch, ImportBatch, RunParameterValue } from './adapter'\nimport { getDataSyncAdapter, resolveProviderKey } from './adapter-registry'\nimport type { SyncRunService } from './sync-run-service'\nimport { SyncRunOwnershipConflictError } from './sync-run-service'\nimport { forEachBatch } from './batch-stream'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n captureTelemetryTrace,\n type TelemetrySpanAttributes,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { SyncRun } from '../data/entities'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-engine' })\n\ntype RunParameters = Record<string, RunParameterValue>\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\ntype EngineDeps = {\n em: EntityManager\n syncRunService: SyncRunService\n integrationCredentialsService: CredentialsService\n integrationLogService: IntegrationLogService\n integrationStateService?: IntegrationStateService\n progressService: ProgressService\n}\n\n/** Repeated on every batch span so a rooted batch trace identifies its run on its own. */\nfunction runSpanAttributes(run: SyncRun, providerKey: string, scope: SyncScope): TelemetrySpanAttributes {\n return {\n 'data_sync.run_id': run.id,\n 'data_sync.integration_id': run.integrationId,\n 'data_sync.provider_key': providerKey,\n 'data_sync.entity_type': run.entityType,\n 'data_sync.direction': run.direction,\n 'om.tenant_id': scope.tenantId,\n 'om.organization_id': scope.organizationId,\n }\n}\n\nfunction applyImportCounters(batch: ImportBatch): Pick<Required<SyncCounterDelta>, 'createdCount' | 'updatedCount' | 'skippedCount' | 'failedCount'> {\n let createdCount = 0\n let updatedCount = 0\n let skippedCount = 0\n let failedCount = 0\n\n for (const item of batch.items) {\n if (item.action === 'create') createdCount += 1\n else if (item.action === 'update') updatedCount += 1\n else if (item.action === 'failed') failedCount += 1\n else skippedCount += 1\n }\n\n return { createdCount, updatedCount, skippedCount, failedCount }\n}\n\ntype SyncCounterDelta = {\n createdCount?: number\n updatedCount?: number\n skippedCount?: number\n failedCount?: number\n processedCount: number\n}\n\nfunction applyExportCounters(batch: ExportBatch): SyncCounterDelta {\n let failedCount = 0\n let skippedCount = 0\n let updatedCount = 0\n\n for (const result of batch.results) {\n if (result.status === 'error') failedCount += 1\n else if (result.status === 'skipped') skippedCount += 1\n else updatedCount += 1\n }\n\n return {\n failedCount,\n skippedCount,\n updatedCount,\n processedCount: batch.results.length,\n }\n}\n\n// Adapter batches can legitimately outlast the stale-job sweep window (slow upstream\n// APIs), so the engine must heartbeat while a batch is still being produced. The same\n// tick also polls cancellation, so a cancel lands within one interval instead of waiting\n// out the batch \u2014 sharing this timer rather than adding a second one that would double\n// the per-interval round-trips for the whole life of a run.\nconst HEARTBEAT_TICK_MS = (STALE_JOB_TIMEOUT_SECONDS * 1000) / 4\n\n// Runs `tick` on an interval only while the source iterator is pending, so heartbeats\n// stop the moment the producer dies and genuinely stale jobs still get swept. The outer\n// finally closes the adapter generator on early exits (cancellation, ownership conflict).\n// Our own abort, as opposed to a failure that merely coincided with one. Adapters are told to\n// return rather than throw, but `signal.throwIfAborted()` and an aborted `fetch` both surface as\n// this, and either is a cancellation rather than a fault.\n//\n// Matched structurally on `name` rather than with `instanceof Error`, because those two throw a\n// `DOMException`, and whether that inherits from `Error` depends on the runtime \u2014 it does under\n// bare Node 24 and does NOT under the jest environment this is tested in. An `instanceof` test\n// therefore passes or fails on where the code runs, which is not something cancellation should\n// depend on.\nfunction isAbortError(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'AbortError'\n}\n\nasync function* withHeartbeat<T>(source: AsyncIterable<T>, tick: () => void, intervalMs: number): AsyncGenerator<T, void, undefined> {\n const iterator = source[Symbol.asyncIterator]()\n try {\n while (true) {\n const timer = setInterval(tick, intervalMs)\n let result: IteratorResult<T>\n try {\n result = await iterator.next()\n } finally {\n clearInterval(timer)\n }\n if (result.done) return\n yield result.value\n }\n } finally {\n await iterator.return?.()\n }\n}\n\nexport function createSyncEngine(deps: EngineDeps) {\n const { syncRunService, integrationCredentialsService, integrationLogService, integrationStateService, progressService } = deps\n\n async function resolveMapping(adapter: DataSyncAdapter, entityType: string, scope: SyncScope): Promise<DataMapping> {\n return adapter.getMapping({\n entityType,\n scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },\n })\n }\n\n async function updateProgress(progressJobId: string | null | undefined, processedCount: number, totalCount: number | null, scope: SyncScope): Promise<void> {\n if (!progressJobId) return\n\n await progressService.updateProgress(\n progressJobId,\n {\n processedCount,\n totalCount: totalCount ?? undefined,\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n }\n\n // On redelivery the progress counter must resume where the last delivery left off the\n // same way committedBatches does \u2014 updateProgress writes absolute counts, so starting at\n // zero would regress the visible count. The progress job's own processedCount is the only\n // persisted value already in the engine's unit: `batch.processedCount ?? items.length`,\n // i.e. source records. The run's created/updated/skipped/failed counters count emitted\n // items, which adapters may explode several-per-source-record (Akeneo yields a product\n // plus its variants), so seeding from them would overshoot the total and pin the bar.\n async function seedProcessedCount(progressJobId: string | null | undefined, scope: SyncScope): Promise<number> {\n if (!progressJobId) return 0\n const job = await progressService.getJob(progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n return job?.processedCount ?? 0\n }\n\n function makeHeartbeatTick(progressJobId: string | null | undefined, scope: SyncScope): () => void {\n const touchJobHeartbeat = progressService.touchJobHeartbeat?.bind(progressService)\n if (!progressJobId || !touchJobHeartbeat) return () => {}\n let inFlight = false\n return () => {\n if (inFlight) return\n inFlight = true\n touchJobHeartbeat(progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n .catch((error) => {\n logger.warn('Progress heartbeat failed', {\n progressJobId,\n error: error instanceof Error ? error.message : String(error),\n })\n })\n .finally(() => {\n inFlight = false\n })\n }\n }\n\n // Rides the heartbeat timer, which is the only thing that runs while the adapter is still\n // producing a batch \u2014 the engine's own cancellation check sits in the batch handler and is\n // reached only after a yield. Swallows its own errors because it runs on a timer, where an\n // unhandled rejection is fatal, and stops polling once it has aborted.\n function makeCancellationTick(progressJobId: string | null | undefined, scope: SyncScope, controller: AbortController): () => void {\n if (!progressJobId) return () => {}\n let inFlight = false\n return () => {\n if (inFlight || controller.signal.aborted) return\n inFlight = true\n progressService.isCancellationRequested(progressJobId, scope.tenantId, scope.organizationId)\n .then((cancelled) => {\n if (cancelled) controller.abort()\n })\n .catch((error) => {\n logger.warn('Cancellation poll failed', {\n progressJobId,\n error: error instanceof Error ? error.message : String(error),\n })\n })\n .finally(() => {\n inFlight = false\n })\n }\n }\n\n async function refreshCoverageSnapshots(entityTypes: string[] | undefined, scope: SyncScope): Promise<void> {\n if (!entityTypes || entityTypes.length === 0) return\n\n await Promise.allSettled(\n Array.from(new Set(entityTypes.filter((value) => typeof value === 'string' && value.trim().length > 0)))\n .map((entityType) => refreshCoverageSnapshot(deps.em, {\n entityType,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })),\n )\n }\n\n async function logImportItemFailures(\n runId: string,\n integrationId: string,\n items: ImportBatch['items'],\n scope: SyncScope,\n ): Promise<void> {\n const failedItems = items.filter((item) => item.action === 'failed')\n for (const item of failedItems) {\n const errorMessage = typeof item.data.errorMessage === 'string' && item.data.errorMessage.trim().length > 0\n ? item.data.errorMessage.trim()\n : 'Import item failed'\n const sourceProductUuid = typeof item.data.sourceProductUuid === 'string' && item.data.sourceProductUuid.trim().length > 0\n ? item.data.sourceProductUuid.trim()\n : null\n const sourceIdentifier = typeof item.data.sourceIdentifier === 'string' && item.data.sourceIdentifier.trim().length > 0\n ? item.data.sourceIdentifier.trim()\n : null\n const message = [\n `Failed to import item ${item.externalId}`,\n sourceProductUuid ? `(uuid: ${sourceProductUuid})` : null,\n sourceIdentifier ? `(identifier: ${sourceIdentifier})` : null,\n `: ${errorMessage}`,\n ].filter((part) => part !== null).join(' ')\n\n await integrationLogService.write(\n {\n integrationId,\n runId,\n level: 'error',\n message,\n payload: item.data,\n },\n scope,\n )\n }\n }\n\n async function logExportItemFailures(\n runId: string,\n integrationId: string,\n results: ExportBatch['results'],\n scope: SyncScope,\n ): Promise<void> {\n const failedResults = results.filter((result) => result.status === 'error' && result.error)\n for (const result of failedResults) {\n const label = result.externalId ? `${result.externalId} (id: ${result.localId})` : result.localId\n const errorMessage = result.error!.split('\\n')[0]\n const message = `Failed to export item ${label}: ${errorMessage}`\n\n await integrationLogService.write(\n {\n integrationId,\n runId,\n level: 'error',\n message,\n payload: { kind: 'export-item-failure', summary: result.error },\n },\n scope,\n )\n }\n }\n\n async function writeOperationalLog(params: {\n integrationId: string\n runId: string\n level: 'info' | 'warn' | 'error'\n message: string\n scope: SyncScope\n enabled: boolean\n payload?: Record<string, unknown>\n }): Promise<void> {\n if (!params.enabled) return\n\n await integrationLogService.write(\n {\n integrationId: params.integrationId,\n runId: params.runId,\n level: params.level,\n message: params.message,\n payload: params.payload,\n },\n params.scope,\n )\n }\n\n async function updateOperationalState(params: {\n integrationId: string\n status: 'healthy' | 'degraded' | 'unhealthy'\n scope: SyncScope\n enabled: boolean\n }): Promise<void> {\n if (!params.enabled || !integrationStateService) return\n\n await integrationStateService.upsert(\n params.integrationId,\n {\n lastHealthStatus: params.status,\n lastHealthCheckedAt: new Date(),\n },\n params.scope,\n )\n }\n\n async function finalizeRun(\n runId: string,\n status: 'completed' | 'failed' | 'cancelled',\n scope: SyncScope,\n error?: string,\n operationalTelemetry = false,\n ): Promise<void> {\n const existingRun = await syncRunService.getRun(runId, scope)\n const alreadyFinalizedWithSameStatus = existingRun?.status === status\n && (status === 'completed' || status === 'failed' || status === 'cancelled')\n\n const run = await syncRunService.markStatus(runId, status, scope, error)\n if (!run) return\n\n if (alreadyFinalizedWithSameStatus) {\n return\n }\n\n if (run.status !== status) {\n // `markStatus` refuses a terminal -> different-terminal transition and\n // returns the row unchanged, so the run is already finished under another\n // delivery of this job. Everything below \u2014 the progress job, the\n // operational log and the lifecycle event \u2014 would describe the wrong\n // outcome, and `data_sync.run.failed` is dispatched to tenant webhooks.\n // A displaced worker stays silent instead.\n logger.warn('Skipping finalization of a sync run another worker already finalized', {\n runId,\n requestedStatus: status,\n actualStatus: run.status,\n })\n return\n }\n\n if (run.progressJobId) {\n if (status === 'completed') {\n await progressService.completeJob(\n run.progressJobId,\n {\n resultSummary: {\n createdCount: run.createdCount,\n updatedCount: run.updatedCount,\n skippedCount: run.skippedCount,\n failedCount: run.failedCount,\n batchesCompleted: run.batchesCompleted,\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n } else if (status === 'failed') {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: error ?? 'Sync run failed',\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n } else if (status === 'cancelled') {\n await progressService.markCancelled(\n run.progressJobId,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n }\n }\n\n if (status === 'completed') {\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'healthy',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: 'Sync run completed',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'completed',\n summary: `Sync completed with ${run.createdCount} created, ${run.updatedCount} updated, ${run.failedCount} failed.`,\n createdCount: run.createdCount,\n updatedCount: run.updatedCount,\n skippedCount: run.skippedCount,\n failedCount: run.failedCount,\n batchesCompleted: run.batchesCompleted,\n },\n })\n } else if (status === 'cancelled') {\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'degraded',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'warn',\n message: 'Sync run cancelled',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'cancelled',\n summary: 'The sync run was cancelled before completion.',\n },\n })\n } else {\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'unhealthy',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'error',\n message: error ?? 'Sync run failed',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'failed',\n summary: error ?? 'The sync run failed.',\n },\n })\n }\n\n if (status === 'completed') {\n await emitDataSyncEvent('data_sync.run.completed', {\n runId,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n return\n }\n\n if (status === 'cancelled') {\n await emitDataSyncEvent('data_sync.run.cancelled', {\n runId,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n return\n }\n\n await emitDataSyncEvent('data_sync.run.failed', {\n runId,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n error: error ?? null,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n }\n\n return {\n async runImport(runId: string, batchSize: number, scope: SyncScope): Promise<void> {\n const run = await syncRunService.getRun(runId, scope)\n if (!run) {\n logger.warn('Skipping stale import job for missing run', { runId })\n return\n }\n if (run.status === 'cancelled') {\n if (run.progressJobId) {\n await progressService.markCancelled(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n return\n }\n\n const providerKey = resolveProviderKey(run.integrationId)\n const adapter = getDataSyncAdapter(providerKey)\n if (!adapter?.streamImport) {\n throw new Error(`No import adapter registered for provider ${providerKey}`)\n }\n const operationalTelemetry = adapter.operationalTelemetry === true\n const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true\n\n const credentials = await integrationCredentialsService.resolve(run.integrationId, scope)\n if (!credentials) {\n throw new Error(`Integration ${run.integrationId} is missing credentials`)\n }\n\n // A run already `running` means a stalled job was redelivered, so this is\n // a resume rather than a first start. Consumers see one `started` event per\n // delivery either way; the flag is what lets them tell the two apart.\n const resumed = run.status === 'running'\n const activeRun = await syncRunService.markStatus(run.id, 'running', scope)\n if (!activeRun || activeRun.status !== 'running') {\n return\n }\n await emitDataSyncEvent('data_sync.run.started', {\n runId: run.id,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n resumed,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'degraded',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: 'Sync run started',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Import run started for ${run.entityType}.`,\n entityType: run.entityType,\n direction: run.direction,\n },\n })\n\n if (run.progressJobId) {\n await progressService.startJob(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n\n const mapping = await resolveMapping(adapter, run.entityType, scope)\n let processedCount = await seedProcessedCount(run.progressJobId, scope)\n let totalCount: number | null = null\n let committedBatches = activeRun.batchesCompleted ?? 0\n // Whether the last committed batch said the source was exhausted. Distinguishes a stream that\n // drained from one the adapter stopped early \u2014 see the post-stream finalize below.\n let streamReportedDone = false\n // Captured while the triggering job's span is still the active one, so\n // every rooted batch trace can link back to it.\n const runTrace = captureTelemetryTrace()\n const spanAttributes = runSpanAttributes(run, providerKey, scope)\n // Declared outside the try because both the catch and the completion path below read it.\n const cancellation = new AbortController()\n const heartbeat = makeHeartbeatTick(run.progressJobId, scope)\n const pollCancellation = makeCancellationTick(run.progressJobId, scope, cancellation)\n\n try {\n const streamResult = await forEachBatch(\n withHeartbeat(\n adapter.streamImport({\n entityType: run.entityType,\n cursor: run.cursor ?? undefined,\n batchSize,\n credentials,\n mapping,\n scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },\n runId: run.id,\n parameters: (run.parameters ?? {}) as RunParameters,\n signal: cancellation.signal,\n }),\n // `finally` so a synchronous throw from the heartbeat cannot also stop cancellation\n // from being observed for the rest of the run.\n () => { try { heartbeat() } finally { pollCancellation() } },\n HEARTBEAT_TICK_MS,\n ),\n {\n spanName: 'data_sync.import.batch',\n drainSpanName: 'data_sync.import.drain',\n attributes: spanAttributes,\n linkTo: runTrace,\n },\n async (batch, span) => {\n span.setAttributes({\n 'data_sync.batch_index': batch.batchIndex,\n 'data_sync.batch_size': batch.items.length,\n })\n\n if (cancellation.signal.aborted || (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId))) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return 'stop'\n }\n\n const delta = applyImportCounters(batch)\n const processedBatchCount = batch.processedCount ?? batch.items.length\n processedCount += processedBatchCount\n totalCount = batch.totalEstimate ?? totalCount\n\n span.setAttributes({\n 'data_sync.processed_count': processedBatchCount,\n 'data_sync.created_count': delta.createdCount,\n 'data_sync.updated_count': delta.updatedCount,\n 'data_sync.skipped_count': delta.skippedCount,\n 'data_sync.failed_count': delta.failedCount,\n })\n\n await syncRunService.commitBatchProgress(\n run.id,\n {\n ...delta,\n batchesCompleted: 1,\n },\n batch.cursor,\n scope,\n { expectedBatchesCompleted: committedBatches, persistSharedCursor },\n )\n committedBatches += 1\n streamReportedDone = batch.hasMore === false\n\n await updateProgress(run.progressJobId, processedCount, totalCount, scope)\n await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope)\n await logImportItemFailures(run.id, run.integrationId, batch.items, scope)\n\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: batch.message?.trim().length\n ? batch.message.trim()\n : `Processed import batch ${batch.batchIndex}`,\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Processed ${processedCount}${totalCount ? ` of ${totalCount}` : ''} rows so far.`,\n processedCount,\n batchSize: batch.items.length,\n processedBatchCount,\n cursor: batch.cursor,\n },\n })\n\n return 'continue'\n },\n )\n if (streamResult === 'stopped') return\n } catch (error) {\n if (error instanceof SyncRunOwnershipConflictError) {\n logger.warn('Yielding import run to a concurrent worker that already advanced it', {\n runId: run.id,\n expectedBatchesCompleted: error.expectedBatchesCompleted,\n })\n return\n }\n // An adapter that honours the signal may reject instead of returning, so our own abort is\n // a cancellation rather than a fault \u2014 and must not leave an `error` entry in the\n // integration log for a run the operator cancelled on purpose. Anything else that merely\n // coincided with the cancel \u2014 a rejecting commit, an upstream 500 \u2014 is a genuine failure\n // and keeps its log entry, its message, its `failed` status and its failed event.\n if (cancellation.signal.aborted && isAbortError(error)) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return\n }\n const message = error instanceof Error ? error.message : 'Sync import failed'\n await integrationLogService.write(\n {\n integrationId: run.integrationId,\n runId: run.id,\n level: 'error',\n message,\n },\n scope,\n )\n await finalizeRun(run.id, 'failed', scope, message, operationalTelemetry)\n return\n }\n\n // An adapter that honours the signal stops mid-batch and returns WITHOUT yielding, so the\n // batch handler \u2014 which owns the only other `cancelled` transition \u2014 never runs and the\n // stream reports `completed`.\n //\n // `streamReportedDone` keeps that from swallowing a run that genuinely finished: an adapter\n // that ignores the signal and drains after reporting `hasMore: false` delivered everything it\n // had, even when the cancel landed during the final read. Calling that cancelled would tell\n // the operator a complete sync was partial and leave a finished run resumable.\n if (cancellation.signal.aborted && !streamReportedDone) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return\n }\n\n await finalizeRun(run.id, 'completed', scope, undefined, operationalTelemetry)\n },\n\n async runExport(runId: string, batchSize: number, scope: SyncScope): Promise<void> {\n const run = await syncRunService.getRun(runId, scope)\n if (!run) {\n logger.warn('Skipping stale export job for missing run', { runId })\n return\n }\n if (run.status === 'cancelled') {\n if (run.progressJobId) {\n await progressService.markCancelled(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n return\n }\n\n const providerKey = resolveProviderKey(run.integrationId)\n const adapter = getDataSyncAdapter(providerKey)\n if (!adapter?.streamExport) {\n throw new Error(`No export adapter registered for provider ${providerKey}`)\n }\n const operationalTelemetry = adapter.operationalTelemetry === true\n const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true\n\n const credentials = await integrationCredentialsService.resolve(run.integrationId, scope)\n if (!credentials) {\n throw new Error(`Integration ${run.integrationId} is missing credentials`)\n }\n\n // A run already `running` means a stalled job was redelivered, so this is\n // a resume rather than a first start. Consumers see one `started` event per\n // delivery either way; the flag is what lets them tell the two apart.\n const resumed = run.status === 'running'\n const activeRun = await syncRunService.markStatus(run.id, 'running', scope)\n if (!activeRun || activeRun.status !== 'running') {\n return\n }\n await emitDataSyncEvent('data_sync.run.started', {\n runId: run.id,\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n resumed,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n await updateOperationalState({\n integrationId: run.integrationId,\n status: 'degraded',\n scope,\n enabled: operationalTelemetry,\n })\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: 'Sync run started',\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Export run started for ${run.entityType}.`,\n entityType: run.entityType,\n direction: run.direction,\n },\n })\n\n if (run.progressJobId) {\n await progressService.startJob(run.progressJobId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n })\n }\n\n const mapping = await resolveMapping(adapter, run.entityType, scope)\n let processedCount = await seedProcessedCount(run.progressJobId, scope)\n let committedBatches = activeRun.batchesCompleted ?? 0\n // Whether the last committed batch said the source was exhausted. Distinguishes a stream that\n // drained from one the adapter stopped early \u2014 see the post-stream finalize below.\n let streamReportedDone = false\n // Captured while the triggering job's span is still the active one, so\n // every rooted batch trace can link back to it.\n const runTrace = captureTelemetryTrace()\n const spanAttributes = runSpanAttributes(run, providerKey, scope)\n // Declared outside the try because both the catch and the completion path below read it.\n const cancellation = new AbortController()\n const heartbeat = makeHeartbeatTick(run.progressJobId, scope)\n const pollCancellation = makeCancellationTick(run.progressJobId, scope, cancellation)\n\n try {\n const streamResult = await forEachBatch(\n withHeartbeat(\n adapter.streamExport({\n entityType: run.entityType,\n cursor: run.cursor ?? undefined,\n batchSize,\n credentials,\n mapping,\n scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },\n runId: run.id,\n parameters: (run.parameters ?? {}) as RunParameters,\n signal: cancellation.signal,\n }),\n // `finally` so a synchronous throw from the heartbeat cannot also stop cancellation\n // from being observed for the rest of the run.\n () => { try { heartbeat() } finally { pollCancellation() } },\n HEARTBEAT_TICK_MS,\n ),\n {\n spanName: 'data_sync.export.batch',\n drainSpanName: 'data_sync.export.drain',\n attributes: spanAttributes,\n linkTo: runTrace,\n },\n async (batch, span) => {\n span.setAttributes({\n 'data_sync.batch_index': batch.batchIndex,\n 'data_sync.batch_size': batch.results.length,\n })\n\n if (cancellation.signal.aborted || (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId))) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return 'stop'\n }\n\n const delta = applyExportCounters(batch)\n processedCount += delta.processedCount\n\n span.setAttributes({\n 'data_sync.processed_count': delta.processedCount,\n 'data_sync.updated_count': delta.updatedCount,\n 'data_sync.skipped_count': delta.skippedCount,\n 'data_sync.failed_count': delta.failedCount,\n })\n\n await syncRunService.commitBatchProgress(\n run.id,\n {\n createdCount: 0,\n updatedCount: delta.updatedCount,\n skippedCount: delta.skippedCount,\n failedCount: delta.failedCount,\n batchesCompleted: 1,\n },\n batch.cursor,\n scope,\n { expectedBatchesCompleted: committedBatches, persistSharedCursor },\n )\n committedBatches += 1\n streamReportedDone = batch.hasMore === false\n await updateProgress(run.progressJobId, processedCount, null, scope)\n await logExportItemFailures(run.id, run.integrationId, batch.results, scope)\n\n await writeOperationalLog({\n integrationId: run.integrationId,\n runId: run.id,\n level: 'info',\n message: `Processed export batch ${batch.batchIndex}`,\n scope,\n enabled: operationalTelemetry,\n payload: {\n operationalStatus: 'running',\n summary: `Processed ${processedCount} export items so far.`,\n processedCount,\n batchSize: batch.results.length,\n cursor: batch.cursor,\n },\n })\n\n return 'continue'\n },\n )\n if (streamResult === 'stopped') return\n } catch (error) {\n if (error instanceof SyncRunOwnershipConflictError) {\n logger.warn('Yielding export run to a concurrent worker that already advanced it', {\n runId: run.id,\n expectedBatchesCompleted: error.expectedBatchesCompleted,\n })\n return\n }\n // An adapter that honours the signal may reject instead of returning, so our own abort is\n // a cancellation rather than a fault \u2014 and must not leave an `error` entry in the\n // integration log for a run the operator cancelled on purpose. Anything else that merely\n // coincided with the cancel \u2014 a rejecting commit, an upstream 500 \u2014 is a genuine failure\n // and keeps its log entry, its message, its `failed` status and its failed event.\n if (cancellation.signal.aborted && isAbortError(error)) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return\n }\n const message = error instanceof Error ? error.message : 'Sync export failed'\n await integrationLogService.write(\n {\n integrationId: run.integrationId,\n runId: run.id,\n level: 'error',\n message,\n },\n scope,\n )\n await finalizeRun(run.id, 'failed', scope, message, operationalTelemetry)\n return\n }\n\n // An adapter that honours the signal stops mid-batch and returns WITHOUT yielding, so the\n // batch handler \u2014 which owns the only other `cancelled` transition \u2014 never runs and the\n // stream reports `completed`.\n //\n // `streamReportedDone` keeps that from swallowing a run that genuinely finished: an adapter\n // that ignores the signal and drains after reporting `hasMore: false` delivered everything it\n // had, even when the cancel landed during the final read. Calling that cancelled would tell\n // the operator a complete sync was partial and leave a finished run resumable.\n if (cancellation.signal.aborted && !streamReportedDone) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return\n }\n\n await finalizeRun(run.id, 'completed', scope, undefined, operationalTelemetry)\n },\n }\n}\n\nexport type SyncEngine = ReturnType<typeof createSyncEngine>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -809,6 +809,7 @@ const syncExcelCustomersAdapter = {
|
|
|
809
809
|
const batchRows = batch.rows;
|
|
810
810
|
const items = [];
|
|
811
811
|
for (let index = 0; index < batchRows.length; index += 1) {
|
|
812
|
+
if (input.signal?.aborted) return;
|
|
812
813
|
items.push(await processRow({
|
|
813
814
|
row: batchRows[index],
|
|
814
815
|
rowNumber: batch.rowStart + index + 1,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/sync_excel/lib/adapters/customers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { extractPhoneDigits, validatePhoneNumber } from '@open-mercato/shared/lib/phone'\nimport type {\n DataMapping,\n DataSyncAdapter,\n FieldMapping,\n ImportBatch,\n ImportItem,\n TenantScope,\n} from '../../../data_sync/lib/adapter'\nimport type { ExternalIdMappingService } from '../../../data_sync/lib/id-mapping'\nimport { SyncMapping } from '../../../data_sync/data/entities'\nimport { CustomerAddress, CustomerEntity } from '../../../customers/data/entities'\nimport { Attachment } from '../../../attachments/data/entities'\nimport { CustomFieldDef } from '../../../entities/data/entities'\nimport { SyncExcelUpload } from '../../data/entities'\nimport { parseCsvDocumentBatches, parseCsvStreamMetadata, type CsvPreviewRow } from '../parser'\nimport { createSyncExcelUploadReadStream } from '../upload-storage'\nimport { E } from '#generated/entities.ids.generated'\n\ntype SyncExcelCursor = {\n uploadId: string\n offset: number\n}\n\ntype Container = Awaited<ReturnType<typeof createRequestContainer>>\n\nconst CUSTOMER_IMPORT_COVERAGE_ENTITY_TYPES = [\n E.customers.customer_entity,\n E.customers.customer_person_profile,\n E.customers.customer_address,\n]\n\ntype PersonFieldValues = {\n externalId?: string | null\n firstName?: string | null\n lastName?: string | null\n displayName?: string | null\n primaryEmail?: string | null\n primaryPhone?: string | null\n jobTitle?: string | null\n status?: string | null\n source?: string | null\n description?: string | null\n}\n\ntype AddressFieldValues = {\n name?: string | null\n purpose?: string | null\n companyName?: string | null\n addressLine1?: string | null\n addressLine2?: string | null\n buildingNumber?: string | null\n flatNumber?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n country?: string | null\n latitude?: number | null\n longitude?: number | null\n}\n\ntype PersonRowValues = {\n values: PersonFieldValues\n customFields: Record<string, unknown>\n addressValues: AddressFieldValues\n}\n\ntype ImportCustomFieldDefinition = {\n key: string\n kind: string\n entityId: string\n organizationId?: string | null\n tenantId?: string | null\n updatedAt?: Date | string | null\n}\n\ntype EmailDedupeIndex = Map<string, string>\n\ntype BuiltPersonPayload = {\n values: PersonFieldValues\n customFields: Record<string, unknown>\n addressValues: AddressFieldValues\n createInput: {\n organizationId: string\n tenantId: string\n firstName: string\n lastName: string\n displayName: string\n primaryEmail?: string\n primaryPhone?: string\n jobTitle?: string\n status?: string\n source?: string\n description?: string\n customFields?: Record<string, unknown>\n } | null\n updateInput: {\n organizationId: string\n tenantId: string\n primaryEmail?: string\n primaryPhone?: string\n jobTitle?: string\n status?: string\n source?: string\n description?: string\n firstName?: string\n lastName?: string\n displayName?: string\n customFields?: Record<string, unknown>\n }\n sourceIdentifier: string | null\n}\n\nfunction normalizeOptionalString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeEmail(value: unknown): string | null {\n const normalized = normalizeOptionalString(value)\n return normalized ? normalized.toLowerCase() : null\n}\n\nconst COUNTRY_DIAL_CODES: Record<string, string> = {\n canada: '1',\n england: '44',\n greatbritain: '44',\n poland: '48',\n scotland: '44',\n uk: '44',\n unitedkingdom: '44',\n unitedstates: '1',\n unitedstatesofamerica: '1',\n usa: '1',\n wales: '44',\n}\n\nfunction normalizeCountryKey(value: unknown): string | null {\n const normalized = normalizeOptionalString(value)\n if (!normalized) return null\n return normalized.toLowerCase().replace(/[^a-z]/g, '')\n}\n\nfunction resolveCountryDialCode(row: CsvPreviewRow): string | null {\n const countryCandidates = [\n row.Country,\n row.country,\n row['Country/Region'],\n row['country/region'],\n ]\n\n for (const candidate of countryCandidates) {\n const countryKey = normalizeCountryKey(candidate)\n if (!countryKey) continue\n const dialCode = COUNTRY_DIAL_CODES[countryKey]\n if (dialCode) return dialCode\n }\n\n return null\n}\n\nfunction stripPhoneExtension(value: string): string {\n return value\n .replace(/\\s*(ext\\.?|extension|x)\\s*\\d+$/i, '')\n .replace(/^\\+\\s+/, '+')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\nfunction normalizeImportedPhone(value: unknown, row: CsvPreviewRow): string | null {\n const normalized = normalizeOptionalString(value)\n if (!normalized) return null\n\n const directValidation = validatePhoneNumber(normalized)\n if (directValidation.valid) {\n return directValidation.normalized\n }\n\n const sanitized = stripPhoneExtension(normalized)\n const sanitizedValidation = validatePhoneNumber(sanitized)\n if (sanitizedValidation.valid) {\n return sanitizedValidation.normalized\n }\n\n const digits = extractPhoneDigits(sanitized)\n if (!digits) return null\n\n if (digits.length >= 9 && digits.length <= 15 && sanitized.startsWith('00')) {\n const internationalCandidate = `+${digits.slice(2)}`\n const internationalValidation = validatePhoneNumber(internationalCandidate)\n if (internationalValidation.valid) {\n return internationalValidation.normalized\n }\n }\n\n const dialCode = resolveCountryDialCode(row)\n if (!dialCode) return null\n\n let nationalDigits = digits\n if (nationalDigits.startsWith(dialCode)) {\n nationalDigits = nationalDigits.slice(dialCode.length)\n } else {\n nationalDigits = nationalDigits.replace(/^0+/, '')\n }\n\n if (!nationalDigits) return null\n\n const localizedCandidate = `+${dialCode}${nationalDigits}`\n const localizedValidation = validatePhoneNumber(localizedCandidate)\n return localizedValidation.valid ? localizedValidation.normalized : null\n}\n\nfunction normalizeOptionalNumber(value: unknown): number | null {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : null\n }\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (trimmed.length === 0) return null\n const parsed = Number(trimmed)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction normalizeCustomFieldDate(value: string): string | null {\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return null\n return parsed.toISOString().slice(0, 10)\n}\n\nfunction normalizeCustomFieldBoolean(value: string): boolean | null {\n const normalized = value.trim().toLowerCase()\n if (['true', '1', 'yes', 'y', 't', 'tak'].includes(normalized)) return true\n if (['false', '0', 'no', 'n', 'f', 'nie'].includes(normalized)) return false\n return null\n}\n\nfunction coerceCustomFieldValue(\n key: string,\n value: unknown,\n definitions: Map<string, ImportCustomFieldDefinition>,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (typeof value === 'string' && value.trim().length === 0) {\n return { ok: true, value: null }\n }\n\n const definition = definitions.get(key)\n if (!definition) {\n return { ok: true, value: typeof value === 'string' ? value.trim() : value }\n }\n\n if (value === null || value === undefined) {\n return { ok: true, value: null }\n }\n\n const text = typeof value === 'string' ? value.trim() : String(value)\n if (text.length === 0) {\n return { ok: true, value: null }\n }\n\n const kind = definition.kind.toLowerCase()\n if (kind === 'boolean') {\n const booleanValue = normalizeCustomFieldBoolean(text)\n if (booleanValue === null) {\n return { ok: false, error: `Custom field \"${key}\" expects a boolean value.` }\n }\n return { ok: true, value: booleanValue }\n }\n\n if (kind === 'integer') {\n const numberValue = Number(text)\n if (!Number.isInteger(numberValue)) {\n return { ok: false, error: `Custom field \"${key}\" expects an integer value.` }\n }\n return { ok: true, value: numberValue }\n }\n\n if (kind === 'float' || kind === 'currency' || kind === 'number') {\n const numberValue = Number(text)\n if (!Number.isFinite(numberValue)) {\n return { ok: false, error: `Custom field \"${key}\" expects a number value.` }\n }\n return { ok: true, value: numberValue }\n }\n\n if (kind === 'date' || kind === 'datetime') {\n const dateValue = normalizeCustomFieldDate(text)\n if (!dateValue) {\n return { ok: false, error: `Custom field \"${key}\" expects a date value.` }\n }\n return { ok: true, value: dateValue }\n }\n\n return { ok: true, value: text }\n}\n\nfunction coerceCustomFields(\n values: Record<string, unknown>,\n definitions: Map<string, ImportCustomFieldDefinition>,\n): { ok: true; values: Record<string, unknown> } | { ok: false; error: string } {\n const coerced: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n const result = coerceCustomFieldValue(key, value, definitions)\n if (!result.ok) return result\n coerced[key] = result.value\n }\n return { ok: true, values: coerced }\n}\n\nfunction customFieldDefinitionScopeScore(definition: ImportCustomFieldDefinition): number {\n return (definition.tenantId ? 2 : 0) + (definition.organizationId ? 1 : 0)\n}\n\nfunction customFieldDefinitionUpdatedAt(definition: ImportCustomFieldDefinition): number {\n if (!definition.updatedAt) return 0\n const value = definition.updatedAt instanceof Date\n ? definition.updatedAt.getTime()\n : new Date(definition.updatedAt).getTime()\n return Number.isFinite(value) ? value : 0\n}\n\nfunction preferCustomFieldDefinition(\n current: ImportCustomFieldDefinition | undefined,\n candidate: ImportCustomFieldDefinition,\n): boolean {\n if (!current) return true\n const candidateScore = customFieldDefinitionScopeScore(candidate)\n const currentScore = customFieldDefinitionScopeScore(current)\n if (candidateScore !== currentScore) return candidateScore > currentScore\n return customFieldDefinitionUpdatedAt(candidate) >= customFieldDefinitionUpdatedAt(current)\n}\n\nasync function loadImportCustomFieldDefinitions(\n em: EntityManager,\n scope: TenantScope,\n): Promise<Map<string, ImportCustomFieldDefinition>> {\n const definitions = await findWithDecryption(\n em,\n CustomFieldDef,\n {\n entityId: { $in: [E.customers.customer_entity, E.customers.customer_person_profile] as string[] },\n deletedAt: null,\n isActive: true,\n $and: [\n { $or: [{ tenantId: scope.tenantId }, { tenantId: null }] },\n { $or: [{ organizationId: scope.organizationId }, { organizationId: null }] },\n ],\n },\n undefined,\n scope,\n )\n\n const byKey = new Map<string, ImportCustomFieldDefinition>()\n for (const definition of definitions) {\n const candidate: ImportCustomFieldDefinition = {\n key: definition.key,\n kind: definition.kind,\n entityId: definition.entityId,\n organizationId: definition.organizationId ?? null,\n tenantId: definition.tenantId ?? null,\n updatedAt: definition.updatedAt ?? null,\n }\n if (preferCustomFieldDefinition(byKey.get(candidate.key), candidate)) {\n byKey.set(candidate.key, candidate)\n }\n }\n return byKey\n}\n\nfunction buildCommandContext(container: Container, scope: TenantScope): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: {\n selectedId: scope.organizationId,\n filterIds: [scope.organizationId],\n allowedIds: [scope.organizationId],\n tenantId: scope.tenantId,\n },\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n}\n\nexport function createCursor(uploadId: string, offset: number): string {\n return JSON.stringify({\n uploadId,\n offset,\n })\n}\n\nexport function parseCursor(value: string | null | undefined): SyncExcelCursor | null {\n if (!value) return null\n try {\n const parsed = JSON.parse(value) as Partial<SyncExcelCursor> | null\n if (!parsed || typeof parsed !== 'object') return null\n if (typeof parsed.uploadId !== 'string' || parsed.uploadId.trim().length === 0) return null\n if (typeof parsed.offset !== 'number' || !Number.isFinite(parsed.offset) || parsed.offset < 0) return null\n return {\n uploadId: parsed.uploadId,\n offset: parsed.offset,\n }\n } catch {\n return null\n }\n}\n\nfunction mapRowValues(row: CsvPreviewRow, fields: FieldMapping[]): PersonRowValues {\n const values: PersonFieldValues = {}\n const customFields: Record<string, unknown> = {}\n const addressValues: AddressFieldValues = {}\n\n for (const field of fields) {\n if (field.mappingKind === 'ignore') continue\n const rawValue = row[field.externalField]\n if (rawValue === undefined || rawValue === null) continue\n if (field.mappingKind === 'custom_field' || field.localField.startsWith('cf:')) {\n const customFieldKey = field.localField.startsWith('cf:') ? field.localField.slice(3) : field.localField\n if (customFieldKey.trim().length > 0) {\n customFields[customFieldKey] = rawValue\n }\n continue\n }\n\n if (field.localField === 'person.externalId') {\n values.externalId = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.firstName') {\n values.firstName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.lastName') {\n values.lastName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.displayName') {\n values.displayName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.primaryEmail') {\n values.primaryEmail = normalizeEmail(rawValue)\n continue\n }\n if (field.localField === 'person.primaryPhone') {\n values.primaryPhone = normalizeImportedPhone(rawValue, row)\n continue\n }\n if (field.localField === 'person.jobTitle') {\n values.jobTitle = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.status') {\n values.status = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.source') {\n values.source = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.description') {\n values.description = normalizeOptionalString(rawValue)\n continue\n }\n\n if (field.localField === 'address.name') {\n addressValues.name = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.purpose') {\n addressValues.purpose = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.companyName') {\n addressValues.companyName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.addressLine1') {\n addressValues.addressLine1 = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.addressLine2') {\n addressValues.addressLine2 = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.buildingNumber') {\n addressValues.buildingNumber = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.flatNumber') {\n addressValues.flatNumber = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.city') {\n addressValues.city = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.region') {\n addressValues.region = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.postalCode') {\n addressValues.postalCode = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.country') {\n addressValues.country = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.latitude') {\n addressValues.latitude = normalizeOptionalNumber(rawValue)\n continue\n }\n if (field.localField === 'address.longitude') {\n addressValues.longitude = normalizeOptionalNumber(rawValue)\n }\n }\n\n return {\n values,\n customFields,\n addressValues,\n }\n}\n\nfunction derivePersonNames(values: PersonFieldValues): { firstName: string; lastName: string; displayName: string } | null {\n const firstName = values.firstName ?? null\n const lastName = values.lastName ?? null\n const explicitDisplayName = values.displayName ?? null\n\n if (firstName && lastName) {\n return {\n firstName,\n lastName,\n displayName: explicitDisplayName ?? `${firstName} ${lastName}`.trim(),\n }\n }\n\n if (explicitDisplayName) {\n const parts = explicitDisplayName.split(/\\s+/).filter((part) => part.length > 0)\n if (parts.length >= 2) {\n return {\n firstName: firstName ?? parts.slice(0, -1).join(' '),\n lastName: lastName ?? parts.at(-1) ?? explicitDisplayName,\n displayName: explicitDisplayName,\n }\n }\n return {\n firstName: firstName ?? explicitDisplayName,\n lastName: lastName ?? explicitDisplayName,\n displayName: explicitDisplayName,\n }\n }\n\n return null\n}\n\nexport function buildPersonPayload(row: CsvPreviewRow, mapping: DataMapping, scope: TenantScope): BuiltPersonPayload {\n const { values, customFields, addressValues } = mapRowValues(row, mapping.fields)\n const derivedNames = derivePersonNames(values)\n const sourceIdentifier = values.externalId ?? values.primaryEmail ?? values.displayName ?? null\n\n const updateInput: BuiltPersonPayload['updateInput'] = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n\n if (values.primaryEmail) updateInput.primaryEmail = values.primaryEmail\n if (values.primaryPhone) updateInput.primaryPhone = values.primaryPhone\n if (values.jobTitle) updateInput.jobTitle = values.jobTitle\n if (values.status) updateInput.status = values.status\n if (values.source) updateInput.source = values.source\n if (values.description) updateInput.description = values.description\n if (derivedNames?.firstName) updateInput.firstName = derivedNames.firstName\n if (derivedNames?.lastName) updateInput.lastName = derivedNames.lastName\n if (derivedNames?.displayName) updateInput.displayName = derivedNames.displayName\n\n if (!derivedNames) {\n return {\n values,\n customFields,\n addressValues,\n createInput: null,\n updateInput,\n sourceIdentifier,\n }\n }\n\n return {\n values,\n customFields,\n addressValues,\n createInput: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n firstName: derivedNames.firstName,\n lastName: derivedNames.lastName,\n displayName: derivedNames.displayName,\n ...(values.primaryEmail ? { primaryEmail: values.primaryEmail } : {}),\n ...(values.primaryPhone ? { primaryPhone: values.primaryPhone } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.status ? { status: values.status } : {}),\n ...(values.source ? { source: values.source } : {}),\n ...(values.description ? { description: values.description } : {}),\n },\n updateInput,\n sourceIdentifier,\n }\n}\n\nfunction hasAddressValues(addressValues: AddressFieldValues): boolean {\n return Object.values(addressValues).some((value) => value !== null && value !== undefined)\n}\n\nfunction buildAddressCreateInput(addressValues: AddressFieldValues, entityId: string, scope: TenantScope) {\n if (!addressValues.addressLine1) return null\n\n return {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n entityId,\n addressLine1: addressValues.addressLine1,\n isPrimary: true,\n ...(addressValues.name ? { name: addressValues.name } : {}),\n ...(addressValues.purpose ? { purpose: addressValues.purpose } : {}),\n ...(addressValues.companyName ? { companyName: addressValues.companyName } : {}),\n ...(addressValues.addressLine2 ? { addressLine2: addressValues.addressLine2 } : {}),\n ...(addressValues.buildingNumber ? { buildingNumber: addressValues.buildingNumber } : {}),\n ...(addressValues.flatNumber ? { flatNumber: addressValues.flatNumber } : {}),\n ...(addressValues.city ? { city: addressValues.city } : {}),\n ...(addressValues.region ? { region: addressValues.region } : {}),\n ...(addressValues.postalCode ? { postalCode: addressValues.postalCode } : {}),\n ...(addressValues.country ? { country: addressValues.country } : {}),\n ...(addressValues.latitude !== null && addressValues.latitude !== undefined ? { latitude: addressValues.latitude } : {}),\n ...(addressValues.longitude !== null && addressValues.longitude !== undefined ? { longitude: addressValues.longitude } : {}),\n }\n}\n\nasync function upsertPrimaryAddress(params: {\n entityId: string\n addressValues: AddressFieldValues\n scope: TenantScope\n commandBus: CommandBus\n commandContext: CommandRuntimeContext\n em: EntityManager\n}): Promise<void> {\n if (!hasAddressValues(params.addressValues)) return\n if (!params.addressValues.addressLine1) return\n\n const [existingPrimaryAddress] = await findWithDecryption(\n params.em,\n CustomerAddress,\n {\n entity: params.entityId,\n organizationId: params.scope.organizationId,\n tenantId: params.scope.tenantId,\n isPrimary: true,\n },\n {\n orderBy: {\n createdAt: 'asc',\n },\n limit: 1,\n },\n params.scope,\n )\n\n if (existingPrimaryAddress) {\n await params.commandBus.execute('customers.addresses.update', {\n input: {\n id: existingPrimaryAddress.id,\n isPrimary: true,\n ...(params.addressValues.name ? { name: params.addressValues.name } : {}),\n ...(params.addressValues.purpose ? { purpose: params.addressValues.purpose } : {}),\n ...(params.addressValues.companyName ? { companyName: params.addressValues.companyName } : {}),\n ...(params.addressValues.addressLine1 ? { addressLine1: params.addressValues.addressLine1 } : {}),\n ...(params.addressValues.addressLine2 ? { addressLine2: params.addressValues.addressLine2 } : {}),\n ...(params.addressValues.buildingNumber ? { buildingNumber: params.addressValues.buildingNumber } : {}),\n ...(params.addressValues.flatNumber ? { flatNumber: params.addressValues.flatNumber } : {}),\n ...(params.addressValues.city ? { city: params.addressValues.city } : {}),\n ...(params.addressValues.region ? { region: params.addressValues.region } : {}),\n ...(params.addressValues.postalCode ? { postalCode: params.addressValues.postalCode } : {}),\n ...(params.addressValues.country ? { country: params.addressValues.country } : {}),\n ...(params.addressValues.latitude !== null && params.addressValues.latitude !== undefined\n ? { latitude: params.addressValues.latitude }\n : {}),\n ...(params.addressValues.longitude !== null && params.addressValues.longitude !== undefined\n ? { longitude: params.addressValues.longitude }\n : {}),\n },\n ctx: params.commandContext,\n })\n return\n }\n\n const createInput = buildAddressCreateInput(params.addressValues, params.entityId, params.scope)\n if (!createInput) return\n\n await params.commandBus.execute('customers.addresses.create', {\n input: createInput,\n ctx: params.commandContext,\n })\n}\n\nasync function loadStoredMapping(em: EntityManager, entityType: string, scope: TenantScope): Promise<DataMapping> {\n const stored = await findOneWithDecryption(\n em,\n SyncMapping,\n {\n integrationId: 'sync_excel',\n entityType,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n if (stored?.mapping && typeof stored.mapping === 'object') {\n return stored.mapping as unknown as DataMapping\n }\n\n return {\n entityType,\n fields: [],\n matchStrategy: 'custom',\n }\n}\n\nasync function resolveUpload(em: EntityManager, runId: string | undefined, cursor: SyncExcelCursor | null, scope: TenantScope): Promise<SyncExcelUpload | null> {\n if (runId) {\n const uploadByRun = await findOneWithDecryption(\n em,\n SyncExcelUpload,\n {\n syncRunId: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n if (uploadByRun) return uploadByRun\n }\n\n if (cursor?.uploadId) {\n return findOneWithDecryption(\n em,\n SyncExcelUpload,\n {\n id: cursor.uploadId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n }\n\n return null\n}\n\nasync function resolveExistingPersonId(params: {\n externalIdMappingService: ExternalIdMappingService\n externalId: string | null | undefined\n email: string | null | undefined\n emailDedupeIndex: EmailDedupeIndex\n scope: TenantScope\n}): Promise<string | null> {\n if (params.externalId) {\n const mappedLocalId = await params.externalIdMappingService.lookupLocalId(\n 'sync_excel',\n 'customers.person',\n params.externalId,\n params.scope,\n )\n if (mappedLocalId) return mappedLocalId\n }\n\n if (!params.email) return null\n return params.emailDedupeIndex.get(params.email) ?? null\n}\n\nfunction mappingHasEmailField(mapping: DataMapping): boolean {\n return mapping.fields.some((field) => field.localField === 'person.primaryEmail')\n}\n\nasync function buildEmailDedupeIndex(params: {\n em: EntityManager\n mapping: DataMapping\n scope: TenantScope\n}): Promise<EmailDedupeIndex> {\n if (!mappingHasEmailField(params.mapping)) return new Map()\n\n const candidates = await findWithDecryption(\n params.em,\n CustomerEntity,\n {\n kind: 'person',\n organizationId: params.scope.organizationId,\n tenantId: params.scope.tenantId,\n deletedAt: null,\n isActive: true,\n },\n {\n orderBy: {\n createdAt: 'asc',\n },\n },\n params.scope,\n )\n\n const index: EmailDedupeIndex = new Map()\n for (const candidate of candidates) {\n const email = normalizeEmail(candidate.primaryEmail)\n if (!email || index.has(email)) continue\n index.set(email, candidate.id)\n }\n return index\n}\n\nfunction isEmptyRow(row: CsvPreviewRow): boolean {\n return !Object.values(row).some((value) => normalizeOptionalString(value))\n}\n\nasync function processRow(params: {\n row: CsvPreviewRow\n rowNumber: number\n mapping: DataMapping\n scope: TenantScope\n commandBus: CommandBus\n commandContext: CommandRuntimeContext\n externalIdMappingService: ExternalIdMappingService\n emailDedupeIndex: EmailDedupeIndex\n customFieldDefinitions: Map<string, ImportCustomFieldDefinition>\n em: EntityManager\n}): Promise<ImportItem> {\n if (isEmptyRow(params.row)) {\n return {\n externalId: `row:${params.rowNumber}`,\n action: 'skip',\n data: {\n rowNumber: params.rowNumber,\n reason: 'empty_row',\n },\n }\n }\n\n const payload = buildPersonPayload(params.row, params.mapping, params.scope)\n const externalId = payload.values.externalId ?? null\n const sourceIdentifier = payload.sourceIdentifier ?? `row:${params.rowNumber}`\n const customFieldResult = coerceCustomFields(payload.customFields, params.customFieldDefinitions)\n if (!customFieldResult.ok) {\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'failed',\n data: {\n rowNumber: params.rowNumber,\n sourceIdentifier,\n errorMessage: customFieldResult.error,\n },\n }\n }\n const existingId = await resolveExistingPersonId({\n externalIdMappingService: params.externalIdMappingService,\n externalId,\n email: payload.values.primaryEmail,\n emailDedupeIndex: params.emailDedupeIndex,\n scope: params.scope,\n })\n\n if (!existingId && !payload.createInput) {\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'failed',\n data: {\n rowNumber: params.rowNumber,\n sourceIdentifier,\n errorMessage: 'Import row is missing a usable person name mapping.',\n },\n }\n }\n\n try {\n if (existingId) {\n const updateInput = {\n id: existingId,\n ...payload.updateInput,\n ...(Object.keys(customFieldResult.values).length > 0 ? { customFields: customFieldResult.values } : {}),\n }\n await params.commandBus.execute('customers.people.update', {\n input: updateInput,\n ctx: params.commandContext,\n })\n\n await upsertPrimaryAddress({\n entityId: existingId,\n addressValues: payload.addressValues,\n scope: params.scope,\n commandBus: params.commandBus,\n commandContext: params.commandContext,\n em: params.em,\n })\n\n if (externalId) {\n await params.externalIdMappingService.storeExternalIdMapping(\n 'sync_excel',\n 'customers.person',\n existingId,\n externalId,\n params.scope,\n )\n }\n if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {\n params.emailDedupeIndex.set(payload.values.primaryEmail, existingId)\n }\n\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'update',\n data: {\n localId: existingId,\n rowNumber: params.rowNumber,\n sourceIdentifier,\n },\n }\n }\n\n const commandResult = await params.commandBus.execute<\n NonNullable<BuiltPersonPayload['createInput']>,\n { entityId: string; personId: string }\n >('customers.people.create', {\n input: {\n ...payload.createInput!,\n ...(Object.keys(customFieldResult.values).length > 0 ? { customFields: customFieldResult.values } : {}),\n },\n ctx: params.commandContext,\n })\n\n await upsertPrimaryAddress({\n entityId: commandResult.result.entityId,\n addressValues: payload.addressValues,\n scope: params.scope,\n commandBus: params.commandBus,\n commandContext: params.commandContext,\n em: params.em,\n })\n\n if (externalId) {\n await params.externalIdMappingService.storeExternalIdMapping(\n 'sync_excel',\n 'customers.person',\n commandResult.result.entityId,\n externalId,\n params.scope,\n )\n }\n if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {\n params.emailDedupeIndex.set(payload.values.primaryEmail, commandResult.result.entityId)\n }\n\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'create',\n data: {\n localId: commandResult.result.entityId,\n personId: commandResult.result.personId,\n rowNumber: params.rowNumber,\n sourceIdentifier,\n },\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Import row failed'\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'failed',\n data: {\n rowNumber: params.rowNumber,\n sourceIdentifier,\n errorMessage,\n },\n }\n }\n}\n\nexport const syncExcelCustomersAdapter: DataSyncAdapter = {\n providerKey: 'excel',\n runMode: 'provider',\n operationalTelemetry: true,\n direction: 'import',\n supportedEntities: ['customers.person'],\n\n async getMapping(input): Promise<DataMapping> {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return loadStoredMapping(em, input.entityType, input.scope)\n },\n\n async *streamImport(input): AsyncIterable<ImportBatch> {\n if (input.entityType !== 'customers.person') {\n throw new Error(`Unsupported sync_excel entity type: ${input.entityType}`)\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const commandBus = container.resolve('commandBus') as CommandBus\n const externalIdMappingService = container.resolve('externalIdMappingService') as ExternalIdMappingService\n const cursor = parseCursor(input.cursor)\n const upload = await resolveUpload(em, input.runId, cursor, input.scope)\n\n if (!upload) {\n throw new Error('CSV upload session was not found for this sync run.')\n }\n\n if (input.runId) {\n upload.syncRunId = input.runId\n }\n upload.status = 'importing'\n await em.flush()\n\n try {\n const attachment = await findOneWithDecryption(\n em,\n Attachment,\n {\n id: upload.attachmentId,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n },\n undefined,\n input.scope,\n )\n\n if (!attachment) {\n throw new Error('CSV upload attachment could not be found.')\n }\n\n const document = await parseCsvStreamMetadata(await createSyncExcelUploadReadStream(attachment))\n const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0\n const commandContext = buildCommandContext(container, input.scope)\n const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope)\n const emailDedupeIndex = await buildEmailDedupeIndex({\n em,\n mapping: input.mapping,\n scope: input.scope,\n })\n let batchIndex = 0\n\n for await (const batch of parseCsvDocumentBatches(await createSyncExcelUploadReadStream(attachment), {\n batchSize: input.batchSize,\n startOffset,\n })) {\n const batchRows = batch.rows\n const items: ImportItem[] = []\n\n for (let index = 0; index < batchRows.length; index += 1) {\n items.push(await processRow({\n row: batchRows[index],\n rowNumber: batch.rowStart + index + 1,\n mapping: input.mapping,\n scope: input.scope,\n commandBus,\n commandContext,\n externalIdMappingService,\n emailDedupeIndex,\n customFieldDefinitions,\n em,\n }))\n }\n\n yield {\n items,\n cursor: createCursor(upload.id, batch.nextOffset),\n hasMore: batch.nextOffset < document.totalRows,\n totalEstimate: document.totalRows,\n processedCount: batchRows.length,\n refreshCoverageEntityTypes: CUSTOMER_IMPORT_COVERAGE_ENTITY_TYPES,\n batchIndex,\n message: `Processed ${batch.nextOffset} of ${document.totalRows} CSV rows`,\n }\n batchIndex += 1\n }\n\n upload.status = 'completed'\n await em.flush()\n } catch (error) {\n upload.status = 'failed'\n await em.flush()\n throw error\n }\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { extractPhoneDigits, validatePhoneNumber } from '@open-mercato/shared/lib/phone'\nimport type {\n DataMapping,\n DataSyncAdapter,\n FieldMapping,\n ImportBatch,\n ImportItem,\n TenantScope,\n} from '../../../data_sync/lib/adapter'\nimport type { ExternalIdMappingService } from '../../../data_sync/lib/id-mapping'\nimport { SyncMapping } from '../../../data_sync/data/entities'\nimport { CustomerAddress, CustomerEntity } from '../../../customers/data/entities'\nimport { Attachment } from '../../../attachments/data/entities'\nimport { CustomFieldDef } from '../../../entities/data/entities'\nimport { SyncExcelUpload } from '../../data/entities'\nimport { parseCsvDocumentBatches, parseCsvStreamMetadata, type CsvPreviewRow } from '../parser'\nimport { createSyncExcelUploadReadStream } from '../upload-storage'\nimport { E } from '#generated/entities.ids.generated'\n\ntype SyncExcelCursor = {\n uploadId: string\n offset: number\n}\n\ntype Container = Awaited<ReturnType<typeof createRequestContainer>>\n\nconst CUSTOMER_IMPORT_COVERAGE_ENTITY_TYPES = [\n E.customers.customer_entity,\n E.customers.customer_person_profile,\n E.customers.customer_address,\n]\n\ntype PersonFieldValues = {\n externalId?: string | null\n firstName?: string | null\n lastName?: string | null\n displayName?: string | null\n primaryEmail?: string | null\n primaryPhone?: string | null\n jobTitle?: string | null\n status?: string | null\n source?: string | null\n description?: string | null\n}\n\ntype AddressFieldValues = {\n name?: string | null\n purpose?: string | null\n companyName?: string | null\n addressLine1?: string | null\n addressLine2?: string | null\n buildingNumber?: string | null\n flatNumber?: string | null\n city?: string | null\n region?: string | null\n postalCode?: string | null\n country?: string | null\n latitude?: number | null\n longitude?: number | null\n}\n\ntype PersonRowValues = {\n values: PersonFieldValues\n customFields: Record<string, unknown>\n addressValues: AddressFieldValues\n}\n\ntype ImportCustomFieldDefinition = {\n key: string\n kind: string\n entityId: string\n organizationId?: string | null\n tenantId?: string | null\n updatedAt?: Date | string | null\n}\n\ntype EmailDedupeIndex = Map<string, string>\n\ntype BuiltPersonPayload = {\n values: PersonFieldValues\n customFields: Record<string, unknown>\n addressValues: AddressFieldValues\n createInput: {\n organizationId: string\n tenantId: string\n firstName: string\n lastName: string\n displayName: string\n primaryEmail?: string\n primaryPhone?: string\n jobTitle?: string\n status?: string\n source?: string\n description?: string\n customFields?: Record<string, unknown>\n } | null\n updateInput: {\n organizationId: string\n tenantId: string\n primaryEmail?: string\n primaryPhone?: string\n jobTitle?: string\n status?: string\n source?: string\n description?: string\n firstName?: string\n lastName?: string\n displayName?: string\n customFields?: Record<string, unknown>\n }\n sourceIdentifier: string | null\n}\n\nfunction normalizeOptionalString(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeEmail(value: unknown): string | null {\n const normalized = normalizeOptionalString(value)\n return normalized ? normalized.toLowerCase() : null\n}\n\nconst COUNTRY_DIAL_CODES: Record<string, string> = {\n canada: '1',\n england: '44',\n greatbritain: '44',\n poland: '48',\n scotland: '44',\n uk: '44',\n unitedkingdom: '44',\n unitedstates: '1',\n unitedstatesofamerica: '1',\n usa: '1',\n wales: '44',\n}\n\nfunction normalizeCountryKey(value: unknown): string | null {\n const normalized = normalizeOptionalString(value)\n if (!normalized) return null\n return normalized.toLowerCase().replace(/[^a-z]/g, '')\n}\n\nfunction resolveCountryDialCode(row: CsvPreviewRow): string | null {\n const countryCandidates = [\n row.Country,\n row.country,\n row['Country/Region'],\n row['country/region'],\n ]\n\n for (const candidate of countryCandidates) {\n const countryKey = normalizeCountryKey(candidate)\n if (!countryKey) continue\n const dialCode = COUNTRY_DIAL_CODES[countryKey]\n if (dialCode) return dialCode\n }\n\n return null\n}\n\nfunction stripPhoneExtension(value: string): string {\n return value\n .replace(/\\s*(ext\\.?|extension|x)\\s*\\d+$/i, '')\n .replace(/^\\+\\s+/, '+')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\nfunction normalizeImportedPhone(value: unknown, row: CsvPreviewRow): string | null {\n const normalized = normalizeOptionalString(value)\n if (!normalized) return null\n\n const directValidation = validatePhoneNumber(normalized)\n if (directValidation.valid) {\n return directValidation.normalized\n }\n\n const sanitized = stripPhoneExtension(normalized)\n const sanitizedValidation = validatePhoneNumber(sanitized)\n if (sanitizedValidation.valid) {\n return sanitizedValidation.normalized\n }\n\n const digits = extractPhoneDigits(sanitized)\n if (!digits) return null\n\n if (digits.length >= 9 && digits.length <= 15 && sanitized.startsWith('00')) {\n const internationalCandidate = `+${digits.slice(2)}`\n const internationalValidation = validatePhoneNumber(internationalCandidate)\n if (internationalValidation.valid) {\n return internationalValidation.normalized\n }\n }\n\n const dialCode = resolveCountryDialCode(row)\n if (!dialCode) return null\n\n let nationalDigits = digits\n if (nationalDigits.startsWith(dialCode)) {\n nationalDigits = nationalDigits.slice(dialCode.length)\n } else {\n nationalDigits = nationalDigits.replace(/^0+/, '')\n }\n\n if (!nationalDigits) return null\n\n const localizedCandidate = `+${dialCode}${nationalDigits}`\n const localizedValidation = validatePhoneNumber(localizedCandidate)\n return localizedValidation.valid ? localizedValidation.normalized : null\n}\n\nfunction normalizeOptionalNumber(value: unknown): number | null {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : null\n }\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (trimmed.length === 0) return null\n const parsed = Number(trimmed)\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction normalizeCustomFieldDate(value: string): string | null {\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return null\n return parsed.toISOString().slice(0, 10)\n}\n\nfunction normalizeCustomFieldBoolean(value: string): boolean | null {\n const normalized = value.trim().toLowerCase()\n if (['true', '1', 'yes', 'y', 't', 'tak'].includes(normalized)) return true\n if (['false', '0', 'no', 'n', 'f', 'nie'].includes(normalized)) return false\n return null\n}\n\nfunction coerceCustomFieldValue(\n key: string,\n value: unknown,\n definitions: Map<string, ImportCustomFieldDefinition>,\n): { ok: true; value: unknown } | { ok: false; error: string } {\n if (typeof value === 'string' && value.trim().length === 0) {\n return { ok: true, value: null }\n }\n\n const definition = definitions.get(key)\n if (!definition) {\n return { ok: true, value: typeof value === 'string' ? value.trim() : value }\n }\n\n if (value === null || value === undefined) {\n return { ok: true, value: null }\n }\n\n const text = typeof value === 'string' ? value.trim() : String(value)\n if (text.length === 0) {\n return { ok: true, value: null }\n }\n\n const kind = definition.kind.toLowerCase()\n if (kind === 'boolean') {\n const booleanValue = normalizeCustomFieldBoolean(text)\n if (booleanValue === null) {\n return { ok: false, error: `Custom field \"${key}\" expects a boolean value.` }\n }\n return { ok: true, value: booleanValue }\n }\n\n if (kind === 'integer') {\n const numberValue = Number(text)\n if (!Number.isInteger(numberValue)) {\n return { ok: false, error: `Custom field \"${key}\" expects an integer value.` }\n }\n return { ok: true, value: numberValue }\n }\n\n if (kind === 'float' || kind === 'currency' || kind === 'number') {\n const numberValue = Number(text)\n if (!Number.isFinite(numberValue)) {\n return { ok: false, error: `Custom field \"${key}\" expects a number value.` }\n }\n return { ok: true, value: numberValue }\n }\n\n if (kind === 'date' || kind === 'datetime') {\n const dateValue = normalizeCustomFieldDate(text)\n if (!dateValue) {\n return { ok: false, error: `Custom field \"${key}\" expects a date value.` }\n }\n return { ok: true, value: dateValue }\n }\n\n return { ok: true, value: text }\n}\n\nfunction coerceCustomFields(\n values: Record<string, unknown>,\n definitions: Map<string, ImportCustomFieldDefinition>,\n): { ok: true; values: Record<string, unknown> } | { ok: false; error: string } {\n const coerced: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n const result = coerceCustomFieldValue(key, value, definitions)\n if (!result.ok) return result\n coerced[key] = result.value\n }\n return { ok: true, values: coerced }\n}\n\nfunction customFieldDefinitionScopeScore(definition: ImportCustomFieldDefinition): number {\n return (definition.tenantId ? 2 : 0) + (definition.organizationId ? 1 : 0)\n}\n\nfunction customFieldDefinitionUpdatedAt(definition: ImportCustomFieldDefinition): number {\n if (!definition.updatedAt) return 0\n const value = definition.updatedAt instanceof Date\n ? definition.updatedAt.getTime()\n : new Date(definition.updatedAt).getTime()\n return Number.isFinite(value) ? value : 0\n}\n\nfunction preferCustomFieldDefinition(\n current: ImportCustomFieldDefinition | undefined,\n candidate: ImportCustomFieldDefinition,\n): boolean {\n if (!current) return true\n const candidateScore = customFieldDefinitionScopeScore(candidate)\n const currentScore = customFieldDefinitionScopeScore(current)\n if (candidateScore !== currentScore) return candidateScore > currentScore\n return customFieldDefinitionUpdatedAt(candidate) >= customFieldDefinitionUpdatedAt(current)\n}\n\nasync function loadImportCustomFieldDefinitions(\n em: EntityManager,\n scope: TenantScope,\n): Promise<Map<string, ImportCustomFieldDefinition>> {\n const definitions = await findWithDecryption(\n em,\n CustomFieldDef,\n {\n entityId: { $in: [E.customers.customer_entity, E.customers.customer_person_profile] as string[] },\n deletedAt: null,\n isActive: true,\n $and: [\n { $or: [{ tenantId: scope.tenantId }, { tenantId: null }] },\n { $or: [{ organizationId: scope.organizationId }, { organizationId: null }] },\n ],\n },\n undefined,\n scope,\n )\n\n const byKey = new Map<string, ImportCustomFieldDefinition>()\n for (const definition of definitions) {\n const candidate: ImportCustomFieldDefinition = {\n key: definition.key,\n kind: definition.kind,\n entityId: definition.entityId,\n organizationId: definition.organizationId ?? null,\n tenantId: definition.tenantId ?? null,\n updatedAt: definition.updatedAt ?? null,\n }\n if (preferCustomFieldDefinition(byKey.get(candidate.key), candidate)) {\n byKey.set(candidate.key, candidate)\n }\n }\n return byKey\n}\n\nfunction buildCommandContext(container: Container, scope: TenantScope): CommandRuntimeContext {\n return {\n container,\n auth: null,\n organizationScope: {\n selectedId: scope.organizationId,\n filterIds: [scope.organizationId],\n allowedIds: [scope.organizationId],\n tenantId: scope.tenantId,\n },\n selectedOrganizationId: scope.organizationId,\n organizationIds: [scope.organizationId],\n }\n}\n\nexport function createCursor(uploadId: string, offset: number): string {\n return JSON.stringify({\n uploadId,\n offset,\n })\n}\n\nexport function parseCursor(value: string | null | undefined): SyncExcelCursor | null {\n if (!value) return null\n try {\n const parsed = JSON.parse(value) as Partial<SyncExcelCursor> | null\n if (!parsed || typeof parsed !== 'object') return null\n if (typeof parsed.uploadId !== 'string' || parsed.uploadId.trim().length === 0) return null\n if (typeof parsed.offset !== 'number' || !Number.isFinite(parsed.offset) || parsed.offset < 0) return null\n return {\n uploadId: parsed.uploadId,\n offset: parsed.offset,\n }\n } catch {\n return null\n }\n}\n\nfunction mapRowValues(row: CsvPreviewRow, fields: FieldMapping[]): PersonRowValues {\n const values: PersonFieldValues = {}\n const customFields: Record<string, unknown> = {}\n const addressValues: AddressFieldValues = {}\n\n for (const field of fields) {\n if (field.mappingKind === 'ignore') continue\n const rawValue = row[field.externalField]\n if (rawValue === undefined || rawValue === null) continue\n if (field.mappingKind === 'custom_field' || field.localField.startsWith('cf:')) {\n const customFieldKey = field.localField.startsWith('cf:') ? field.localField.slice(3) : field.localField\n if (customFieldKey.trim().length > 0) {\n customFields[customFieldKey] = rawValue\n }\n continue\n }\n\n if (field.localField === 'person.externalId') {\n values.externalId = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.firstName') {\n values.firstName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.lastName') {\n values.lastName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.displayName') {\n values.displayName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.primaryEmail') {\n values.primaryEmail = normalizeEmail(rawValue)\n continue\n }\n if (field.localField === 'person.primaryPhone') {\n values.primaryPhone = normalizeImportedPhone(rawValue, row)\n continue\n }\n if (field.localField === 'person.jobTitle') {\n values.jobTitle = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.status') {\n values.status = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.source') {\n values.source = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'person.description') {\n values.description = normalizeOptionalString(rawValue)\n continue\n }\n\n if (field.localField === 'address.name') {\n addressValues.name = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.purpose') {\n addressValues.purpose = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.companyName') {\n addressValues.companyName = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.addressLine1') {\n addressValues.addressLine1 = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.addressLine2') {\n addressValues.addressLine2 = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.buildingNumber') {\n addressValues.buildingNumber = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.flatNumber') {\n addressValues.flatNumber = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.city') {\n addressValues.city = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.region') {\n addressValues.region = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.postalCode') {\n addressValues.postalCode = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.country') {\n addressValues.country = normalizeOptionalString(rawValue)\n continue\n }\n if (field.localField === 'address.latitude') {\n addressValues.latitude = normalizeOptionalNumber(rawValue)\n continue\n }\n if (field.localField === 'address.longitude') {\n addressValues.longitude = normalizeOptionalNumber(rawValue)\n }\n }\n\n return {\n values,\n customFields,\n addressValues,\n }\n}\n\nfunction derivePersonNames(values: PersonFieldValues): { firstName: string; lastName: string; displayName: string } | null {\n const firstName = values.firstName ?? null\n const lastName = values.lastName ?? null\n const explicitDisplayName = values.displayName ?? null\n\n if (firstName && lastName) {\n return {\n firstName,\n lastName,\n displayName: explicitDisplayName ?? `${firstName} ${lastName}`.trim(),\n }\n }\n\n if (explicitDisplayName) {\n const parts = explicitDisplayName.split(/\\s+/).filter((part) => part.length > 0)\n if (parts.length >= 2) {\n return {\n firstName: firstName ?? parts.slice(0, -1).join(' '),\n lastName: lastName ?? parts.at(-1) ?? explicitDisplayName,\n displayName: explicitDisplayName,\n }\n }\n return {\n firstName: firstName ?? explicitDisplayName,\n lastName: lastName ?? explicitDisplayName,\n displayName: explicitDisplayName,\n }\n }\n\n return null\n}\n\nexport function buildPersonPayload(row: CsvPreviewRow, mapping: DataMapping, scope: TenantScope): BuiltPersonPayload {\n const { values, customFields, addressValues } = mapRowValues(row, mapping.fields)\n const derivedNames = derivePersonNames(values)\n const sourceIdentifier = values.externalId ?? values.primaryEmail ?? values.displayName ?? null\n\n const updateInput: BuiltPersonPayload['updateInput'] = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n\n if (values.primaryEmail) updateInput.primaryEmail = values.primaryEmail\n if (values.primaryPhone) updateInput.primaryPhone = values.primaryPhone\n if (values.jobTitle) updateInput.jobTitle = values.jobTitle\n if (values.status) updateInput.status = values.status\n if (values.source) updateInput.source = values.source\n if (values.description) updateInput.description = values.description\n if (derivedNames?.firstName) updateInput.firstName = derivedNames.firstName\n if (derivedNames?.lastName) updateInput.lastName = derivedNames.lastName\n if (derivedNames?.displayName) updateInput.displayName = derivedNames.displayName\n\n if (!derivedNames) {\n return {\n values,\n customFields,\n addressValues,\n createInput: null,\n updateInput,\n sourceIdentifier,\n }\n }\n\n return {\n values,\n customFields,\n addressValues,\n createInput: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n firstName: derivedNames.firstName,\n lastName: derivedNames.lastName,\n displayName: derivedNames.displayName,\n ...(values.primaryEmail ? { primaryEmail: values.primaryEmail } : {}),\n ...(values.primaryPhone ? { primaryPhone: values.primaryPhone } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.status ? { status: values.status } : {}),\n ...(values.source ? { source: values.source } : {}),\n ...(values.description ? { description: values.description } : {}),\n },\n updateInput,\n sourceIdentifier,\n }\n}\n\nfunction hasAddressValues(addressValues: AddressFieldValues): boolean {\n return Object.values(addressValues).some((value) => value !== null && value !== undefined)\n}\n\nfunction buildAddressCreateInput(addressValues: AddressFieldValues, entityId: string, scope: TenantScope) {\n if (!addressValues.addressLine1) return null\n\n return {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n entityId,\n addressLine1: addressValues.addressLine1,\n isPrimary: true,\n ...(addressValues.name ? { name: addressValues.name } : {}),\n ...(addressValues.purpose ? { purpose: addressValues.purpose } : {}),\n ...(addressValues.companyName ? { companyName: addressValues.companyName } : {}),\n ...(addressValues.addressLine2 ? { addressLine2: addressValues.addressLine2 } : {}),\n ...(addressValues.buildingNumber ? { buildingNumber: addressValues.buildingNumber } : {}),\n ...(addressValues.flatNumber ? { flatNumber: addressValues.flatNumber } : {}),\n ...(addressValues.city ? { city: addressValues.city } : {}),\n ...(addressValues.region ? { region: addressValues.region } : {}),\n ...(addressValues.postalCode ? { postalCode: addressValues.postalCode } : {}),\n ...(addressValues.country ? { country: addressValues.country } : {}),\n ...(addressValues.latitude !== null && addressValues.latitude !== undefined ? { latitude: addressValues.latitude } : {}),\n ...(addressValues.longitude !== null && addressValues.longitude !== undefined ? { longitude: addressValues.longitude } : {}),\n }\n}\n\nasync function upsertPrimaryAddress(params: {\n entityId: string\n addressValues: AddressFieldValues\n scope: TenantScope\n commandBus: CommandBus\n commandContext: CommandRuntimeContext\n em: EntityManager\n}): Promise<void> {\n if (!hasAddressValues(params.addressValues)) return\n if (!params.addressValues.addressLine1) return\n\n const [existingPrimaryAddress] = await findWithDecryption(\n params.em,\n CustomerAddress,\n {\n entity: params.entityId,\n organizationId: params.scope.organizationId,\n tenantId: params.scope.tenantId,\n isPrimary: true,\n },\n {\n orderBy: {\n createdAt: 'asc',\n },\n limit: 1,\n },\n params.scope,\n )\n\n if (existingPrimaryAddress) {\n await params.commandBus.execute('customers.addresses.update', {\n input: {\n id: existingPrimaryAddress.id,\n isPrimary: true,\n ...(params.addressValues.name ? { name: params.addressValues.name } : {}),\n ...(params.addressValues.purpose ? { purpose: params.addressValues.purpose } : {}),\n ...(params.addressValues.companyName ? { companyName: params.addressValues.companyName } : {}),\n ...(params.addressValues.addressLine1 ? { addressLine1: params.addressValues.addressLine1 } : {}),\n ...(params.addressValues.addressLine2 ? { addressLine2: params.addressValues.addressLine2 } : {}),\n ...(params.addressValues.buildingNumber ? { buildingNumber: params.addressValues.buildingNumber } : {}),\n ...(params.addressValues.flatNumber ? { flatNumber: params.addressValues.flatNumber } : {}),\n ...(params.addressValues.city ? { city: params.addressValues.city } : {}),\n ...(params.addressValues.region ? { region: params.addressValues.region } : {}),\n ...(params.addressValues.postalCode ? { postalCode: params.addressValues.postalCode } : {}),\n ...(params.addressValues.country ? { country: params.addressValues.country } : {}),\n ...(params.addressValues.latitude !== null && params.addressValues.latitude !== undefined\n ? { latitude: params.addressValues.latitude }\n : {}),\n ...(params.addressValues.longitude !== null && params.addressValues.longitude !== undefined\n ? { longitude: params.addressValues.longitude }\n : {}),\n },\n ctx: params.commandContext,\n })\n return\n }\n\n const createInput = buildAddressCreateInput(params.addressValues, params.entityId, params.scope)\n if (!createInput) return\n\n await params.commandBus.execute('customers.addresses.create', {\n input: createInput,\n ctx: params.commandContext,\n })\n}\n\nasync function loadStoredMapping(em: EntityManager, entityType: string, scope: TenantScope): Promise<DataMapping> {\n const stored = await findOneWithDecryption(\n em,\n SyncMapping,\n {\n integrationId: 'sync_excel',\n entityType,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n if (stored?.mapping && typeof stored.mapping === 'object') {\n return stored.mapping as unknown as DataMapping\n }\n\n return {\n entityType,\n fields: [],\n matchStrategy: 'custom',\n }\n}\n\nasync function resolveUpload(em: EntityManager, runId: string | undefined, cursor: SyncExcelCursor | null, scope: TenantScope): Promise<SyncExcelUpload | null> {\n if (runId) {\n const uploadByRun = await findOneWithDecryption(\n em,\n SyncExcelUpload,\n {\n syncRunId: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n if (uploadByRun) return uploadByRun\n }\n\n if (cursor?.uploadId) {\n return findOneWithDecryption(\n em,\n SyncExcelUpload,\n {\n id: cursor.uploadId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n }\n\n return null\n}\n\nasync function resolveExistingPersonId(params: {\n externalIdMappingService: ExternalIdMappingService\n externalId: string | null | undefined\n email: string | null | undefined\n emailDedupeIndex: EmailDedupeIndex\n scope: TenantScope\n}): Promise<string | null> {\n if (params.externalId) {\n const mappedLocalId = await params.externalIdMappingService.lookupLocalId(\n 'sync_excel',\n 'customers.person',\n params.externalId,\n params.scope,\n )\n if (mappedLocalId) return mappedLocalId\n }\n\n if (!params.email) return null\n return params.emailDedupeIndex.get(params.email) ?? null\n}\n\nfunction mappingHasEmailField(mapping: DataMapping): boolean {\n return mapping.fields.some((field) => field.localField === 'person.primaryEmail')\n}\n\nasync function buildEmailDedupeIndex(params: {\n em: EntityManager\n mapping: DataMapping\n scope: TenantScope\n}): Promise<EmailDedupeIndex> {\n if (!mappingHasEmailField(params.mapping)) return new Map()\n\n const candidates = await findWithDecryption(\n params.em,\n CustomerEntity,\n {\n kind: 'person',\n organizationId: params.scope.organizationId,\n tenantId: params.scope.tenantId,\n deletedAt: null,\n isActive: true,\n },\n {\n orderBy: {\n createdAt: 'asc',\n },\n },\n params.scope,\n )\n\n const index: EmailDedupeIndex = new Map()\n for (const candidate of candidates) {\n const email = normalizeEmail(candidate.primaryEmail)\n if (!email || index.has(email)) continue\n index.set(email, candidate.id)\n }\n return index\n}\n\nfunction isEmptyRow(row: CsvPreviewRow): boolean {\n return !Object.values(row).some((value) => normalizeOptionalString(value))\n}\n\nasync function processRow(params: {\n row: CsvPreviewRow\n rowNumber: number\n mapping: DataMapping\n scope: TenantScope\n commandBus: CommandBus\n commandContext: CommandRuntimeContext\n externalIdMappingService: ExternalIdMappingService\n emailDedupeIndex: EmailDedupeIndex\n customFieldDefinitions: Map<string, ImportCustomFieldDefinition>\n em: EntityManager\n}): Promise<ImportItem> {\n if (isEmptyRow(params.row)) {\n return {\n externalId: `row:${params.rowNumber}`,\n action: 'skip',\n data: {\n rowNumber: params.rowNumber,\n reason: 'empty_row',\n },\n }\n }\n\n const payload = buildPersonPayload(params.row, params.mapping, params.scope)\n const externalId = payload.values.externalId ?? null\n const sourceIdentifier = payload.sourceIdentifier ?? `row:${params.rowNumber}`\n const customFieldResult = coerceCustomFields(payload.customFields, params.customFieldDefinitions)\n if (!customFieldResult.ok) {\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'failed',\n data: {\n rowNumber: params.rowNumber,\n sourceIdentifier,\n errorMessage: customFieldResult.error,\n },\n }\n }\n const existingId = await resolveExistingPersonId({\n externalIdMappingService: params.externalIdMappingService,\n externalId,\n email: payload.values.primaryEmail,\n emailDedupeIndex: params.emailDedupeIndex,\n scope: params.scope,\n })\n\n if (!existingId && !payload.createInput) {\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'failed',\n data: {\n rowNumber: params.rowNumber,\n sourceIdentifier,\n errorMessage: 'Import row is missing a usable person name mapping.',\n },\n }\n }\n\n try {\n if (existingId) {\n const updateInput = {\n id: existingId,\n ...payload.updateInput,\n ...(Object.keys(customFieldResult.values).length > 0 ? { customFields: customFieldResult.values } : {}),\n }\n await params.commandBus.execute('customers.people.update', {\n input: updateInput,\n ctx: params.commandContext,\n })\n\n await upsertPrimaryAddress({\n entityId: existingId,\n addressValues: payload.addressValues,\n scope: params.scope,\n commandBus: params.commandBus,\n commandContext: params.commandContext,\n em: params.em,\n })\n\n if (externalId) {\n await params.externalIdMappingService.storeExternalIdMapping(\n 'sync_excel',\n 'customers.person',\n existingId,\n externalId,\n params.scope,\n )\n }\n if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {\n params.emailDedupeIndex.set(payload.values.primaryEmail, existingId)\n }\n\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'update',\n data: {\n localId: existingId,\n rowNumber: params.rowNumber,\n sourceIdentifier,\n },\n }\n }\n\n const commandResult = await params.commandBus.execute<\n NonNullable<BuiltPersonPayload['createInput']>,\n { entityId: string; personId: string }\n >('customers.people.create', {\n input: {\n ...payload.createInput!,\n ...(Object.keys(customFieldResult.values).length > 0 ? { customFields: customFieldResult.values } : {}),\n },\n ctx: params.commandContext,\n })\n\n await upsertPrimaryAddress({\n entityId: commandResult.result.entityId,\n addressValues: payload.addressValues,\n scope: params.scope,\n commandBus: params.commandBus,\n commandContext: params.commandContext,\n em: params.em,\n })\n\n if (externalId) {\n await params.externalIdMappingService.storeExternalIdMapping(\n 'sync_excel',\n 'customers.person',\n commandResult.result.entityId,\n externalId,\n params.scope,\n )\n }\n if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {\n params.emailDedupeIndex.set(payload.values.primaryEmail, commandResult.result.entityId)\n }\n\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'create',\n data: {\n localId: commandResult.result.entityId,\n personId: commandResult.result.personId,\n rowNumber: params.rowNumber,\n sourceIdentifier,\n },\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Import row failed'\n return {\n externalId: externalId ?? sourceIdentifier,\n action: 'failed',\n data: {\n rowNumber: params.rowNumber,\n sourceIdentifier,\n errorMessage,\n },\n }\n }\n}\n\nexport const syncExcelCustomersAdapter: DataSyncAdapter = {\n providerKey: 'excel',\n runMode: 'provider',\n operationalTelemetry: true,\n direction: 'import',\n supportedEntities: ['customers.person'],\n\n async getMapping(input): Promise<DataMapping> {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return loadStoredMapping(em, input.entityType, input.scope)\n },\n\n async *streamImport(input): AsyncIterable<ImportBatch> {\n if (input.entityType !== 'customers.person') {\n throw new Error(`Unsupported sync_excel entity type: ${input.entityType}`)\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const commandBus = container.resolve('commandBus') as CommandBus\n const externalIdMappingService = container.resolve('externalIdMappingService') as ExternalIdMappingService\n const cursor = parseCursor(input.cursor)\n const upload = await resolveUpload(em, input.runId, cursor, input.scope)\n\n if (!upload) {\n throw new Error('CSV upload session was not found for this sync run.')\n }\n\n if (input.runId) {\n upload.syncRunId = input.runId\n }\n upload.status = 'importing'\n await em.flush()\n\n try {\n const attachment = await findOneWithDecryption(\n em,\n Attachment,\n {\n id: upload.attachmentId,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n },\n undefined,\n input.scope,\n )\n\n if (!attachment) {\n throw new Error('CSV upload attachment could not be found.')\n }\n\n const document = await parseCsvStreamMetadata(await createSyncExcelUploadReadStream(attachment))\n const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0\n const commandContext = buildCommandContext(container, input.scope)\n const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope)\n const emailDedupeIndex = await buildEmailDedupeIndex({\n em,\n mapping: input.mapping,\n scope: input.scope,\n })\n let batchIndex = 0\n\n for await (const batch of parseCsvDocumentBatches(await createSyncExcelUploadReadStream(attachment), {\n batchSize: input.batchSize,\n startOffset,\n })) {\n const batchRows = batch.rows\n const items: ImportItem[] = []\n\n for (let index = 0; index < batchRows.length; index += 1) {\n // Above the yield, so an abandoned page is never yielded and its cursor never committed \u2014\n // the rows applied so far are re-applied on resume, which the replay-safety contract on\n // `streamImport` already requires. Each row goes through the command bus, so a large page\n // is exactly the case where the operator would otherwise wait out the whole batch.\n if (input.signal?.aborted) return\n items.push(await processRow({\n row: batchRows[index],\n rowNumber: batch.rowStart + index + 1,\n mapping: input.mapping,\n scope: input.scope,\n commandBus,\n commandContext,\n externalIdMappingService,\n emailDedupeIndex,\n customFieldDefinitions,\n em,\n }))\n }\n\n yield {\n items,\n cursor: createCursor(upload.id, batch.nextOffset),\n hasMore: batch.nextOffset < document.totalRows,\n totalEstimate: document.totalRows,\n processedCount: batchRows.length,\n refreshCoverageEntityTypes: CUSTOMER_IMPORT_COVERAGE_ENTITY_TYPES,\n batchIndex,\n message: `Processed ${batch.nextOffset} of ${document.totalRows} CSV rows`,\n }\n batchIndex += 1\n }\n\n upload.status = 'completed'\n await em.flush()\n } catch (error) {\n upload.status = 'failed'\n await em.flush()\n throw error\n }\n },\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,8BAA8B;AACvC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,oBAAoB,2BAA2B;AAUxD,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB,sBAAsB;AAChD,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,yBAAyB,8BAAkD;AACpF,SAAS,uCAAuC;AAChD,SAAS,SAAS;AASlB,MAAM,wCAAwC;AAAA,EAC5C,EAAE,UAAU;AAAA,EACZ,EAAE,UAAU;AAAA,EACZ,EAAE,UAAU;AACd;AAmFA,SAAS,wBAAwB,OAA+B;AAC9D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEA,SAAS,eAAe,OAA+B;AACrD,QAAM,aAAa,wBAAwB,KAAK;AAChD,SAAO,aAAa,WAAW,YAAY,IAAI;AACjD;AAEA,MAAM,qBAA6C;AAAA,EACjD,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,IAAI;AAAA,EACJ,eAAe;AAAA,EACf,cAAc;AAAA,EACd,uBAAuB;AAAA,EACvB,KAAK;AAAA,EACL,OAAO;AACT;AAEA,SAAS,oBAAoB,OAA+B;AAC1D,QAAM,aAAa,wBAAwB,KAAK;AAChD,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,WAAW,YAAY,EAAE,QAAQ,WAAW,EAAE;AACvD;AAEA,SAAS,uBAAuB,KAAmC;AACjE,QAAM,oBAAoB;AAAA,IACxB,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI,gBAAgB;AAAA,IACpB,IAAI,gBAAgB;AAAA,EACtB;AAEA,aAAW,aAAa,mBAAmB;AACzC,UAAM,aAAa,oBAAoB,SAAS;AAChD,QAAI,CAAC,WAAY;AACjB,UAAM,WAAW,mBAAmB,UAAU;AAC9C,QAAI,SAAU,QAAO;AAAA,EACvB;AAEA,SAAO;AACT;AAEA,SAAS,oBAAoB,OAAuB;AAClD,SAAO,MACJ,QAAQ,mCAAmC,EAAE,EAC7C,QAAQ,UAAU,GAAG,EACrB,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACV;AAEA,SAAS,uBAAuB,OAAgB,KAAmC;AACjF,QAAM,aAAa,wBAAwB,KAAK;AAChD,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,mBAAmB,oBAAoB,UAAU;AACvD,MAAI,iBAAiB,OAAO;AAC1B,WAAO,iBAAiB;AAAA,EAC1B;AAEA,QAAM,YAAY,oBAAoB,UAAU;AAChD,QAAM,sBAAsB,oBAAoB,SAAS;AACzD,MAAI,oBAAoB,OAAO;AAC7B,WAAO,oBAAoB;AAAA,EAC7B;AAEA,QAAM,SAAS,mBAAmB,SAAS;AAC3C,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI,OAAO,UAAU,KAAK,OAAO,UAAU,MAAM,UAAU,WAAW,IAAI,GAAG;AAC3E,UAAM,yBAAyB,IAAI,OAAO,MAAM,CAAC,CAAC;AAClD,UAAM,0BAA0B,oBAAoB,sBAAsB;AAC1E,QAAI,wBAAwB,OAAO;AACjC,aAAO,wBAAwB;AAAA,IACjC;AAAA,EACF;AAEA,QAAM,WAAW,uBAAuB,GAAG;AAC3C,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI,iBAAiB;AACrB,MAAI,eAAe,WAAW,QAAQ,GAAG;AACvC,qBAAiB,eAAe,MAAM,SAAS,MAAM;AAAA,EACvD,OAAO;AACL,qBAAiB,eAAe,QAAQ,OAAO,EAAE;AAAA,EACnD;AAEA,MAAI,CAAC,eAAgB,QAAO;AAE5B,QAAM,qBAAqB,IAAI,QAAQ,GAAG,cAAc;AACxD,QAAM,sBAAsB,oBAAoB,kBAAkB;AAClE,SAAO,oBAAoB,QAAQ,oBAAoB,aAAa;AACtE;AAEA,SAAS,wBAAwB,OAA+B;AAC9D,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AACA,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,SAAS,OAAO,OAAO;AAC7B,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAEA,SAAS,yBAAyB,OAA8B;AAC9D,QAAM,SAAS,IAAI,KAAK,KAAK;AAC7B,MAAI,OAAO,MAAM,OAAO,QAAQ,CAAC,EAAG,QAAO;AAC3C,SAAO,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE;AACzC;AAEA,SAAS,4BAA4B,OAA+B;AAClE,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,QAAQ,KAAK,OAAO,KAAK,KAAK,KAAK,EAAE,SAAS,UAAU,EAAG,QAAO;AACvE,MAAI,CAAC,SAAS,KAAK,MAAM,KAAK,KAAK,KAAK,EAAE,SAAS,UAAU,EAAG,QAAO;AACvE,SAAO;AACT;AAEA,SAAS,uBACP,KACA,OACA,aAC6D;AAC7D,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,WAAW,GAAG;AAC1D,WAAO,EAAE,IAAI,MAAM,OAAO,KAAK;AAAA,EACjC;AAEA,QAAM,aAAa,YAAY,IAAI,GAAG;AACtC,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,IAAI,MAAM,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,MAAM;AAAA,EAC7E;AAEA,MAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,WAAO,EAAE,IAAI,MAAM,OAAO,KAAK;AAAA,EACjC;AAEA,QAAM,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI,OAAO,KAAK;AACpE,MAAI,KAAK,WAAW,GAAG;AACrB,WAAO,EAAE,IAAI,MAAM,OAAO,KAAK;AAAA,EACjC;AAEA,QAAM,OAAO,WAAW,KAAK,YAAY;AACzC,MAAI,SAAS,WAAW;AACtB,UAAM,eAAe,4BAA4B,IAAI;AACrD,QAAI,iBAAiB,MAAM;AACzB,aAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB,GAAG,6BAA6B;AAAA,IAC9E;AACA,WAAO,EAAE,IAAI,MAAM,OAAO,aAAa;AAAA,EACzC;AAEA,MAAI,SAAS,WAAW;AACtB,UAAM,cAAc,OAAO,IAAI;AAC/B,QAAI,CAAC,OAAO,UAAU,WAAW,GAAG;AAClC,aAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB,GAAG,8BAA8B;AAAA,IAC/E;AACA,WAAO,EAAE,IAAI,MAAM,OAAO,YAAY;AAAA,EACxC;AAEA,MAAI,SAAS,WAAW,SAAS,cAAc,SAAS,UAAU;AAChE,UAAM,cAAc,OAAO,IAAI;AAC/B,QAAI,CAAC,OAAO,SAAS,WAAW,GAAG;AACjC,aAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB,GAAG,4BAA4B;AAAA,IAC7E;AACA,WAAO,EAAE,IAAI,MAAM,OAAO,YAAY;AAAA,EACxC;AAEA,MAAI,SAAS,UAAU,SAAS,YAAY;AAC1C,UAAM,YAAY,yBAAyB,IAAI;AAC/C,QAAI,CAAC,WAAW;AACd,aAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB,GAAG,0BAA0B;AAAA,IAC3E;AACA,WAAO,EAAE,IAAI,MAAM,OAAO,UAAU;AAAA,EACtC;AAEA,SAAO,EAAE,IAAI,MAAM,OAAO,KAAK;AACjC;AAEA,SAAS,mBACP,QACA,aAC8E;AAC9E,QAAM,UAAmC,CAAC;AAC1C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAM,SAAS,uBAAuB,KAAK,OAAO,WAAW;AAC7D,QAAI,CAAC,OAAO,GAAI,QAAO;AACvB,YAAQ,GAAG,IAAI,OAAO;AAAA,EACxB;AACA,SAAO,EAAE,IAAI,MAAM,QAAQ,QAAQ;AACrC;AAEA,SAAS,gCAAgC,YAAiD;AACxF,UAAQ,WAAW,WAAW,IAAI,MAAM,WAAW,iBAAiB,IAAI;AAC1E;AAEA,SAAS,+BAA+B,YAAiD;AACvF,MAAI,CAAC,WAAW,UAAW,QAAO;AAClC,QAAM,QAAQ,WAAW,qBAAqB,OAC1C,WAAW,UAAU,QAAQ,IAC7B,IAAI,KAAK,WAAW,SAAS,EAAE,QAAQ;AAC3C,SAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAC1C;AAEA,SAAS,4BACP,SACA,WACS;AACT,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,iBAAiB,gCAAgC,SAAS;AAChE,QAAM,eAAe,gCAAgC,OAAO;AAC5D,MAAI,mBAAmB,aAAc,QAAO,iBAAiB;AAC7D,SAAO,+BAA+B,SAAS,KAAK,+BAA+B,OAAO;AAC5F;AAEA,eAAe,iCACb,IACA,OACmD;AACnD,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,EAAE,KAAK,CAAC,EAAE,UAAU,iBAAiB,EAAE,UAAU,uBAAuB,EAAc;AAAA,MAChG,WAAW;AAAA,MACX,UAAU;AAAA,MACV,MAAM;AAAA,QACJ,EAAE,KAAK,CAAC,EAAE,UAAU,MAAM,SAAS,GAAG,EAAE,UAAU,KAAK,CAAC,EAAE;AAAA,QAC1D,EAAE,KAAK,CAAC,EAAE,gBAAgB,MAAM,eAAe,GAAG,EAAE,gBAAgB,KAAK,CAAC,EAAE;AAAA,MAC9E;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,QAAQ,oBAAI,IAAyC;AAC3D,aAAW,cAAc,aAAa;AACpC,UAAM,YAAyC;AAAA,MAC7C,KAAK,WAAW;AAAA,MAChB,MAAM,WAAW;AAAA,MACjB,UAAU,WAAW;AAAA,MACrB,gBAAgB,WAAW,kBAAkB;AAAA,MAC7C,UAAU,WAAW,YAAY;AAAA,MACjC,WAAW,WAAW,aAAa;AAAA,IACrC;AACA,QAAI,4BAA4B,MAAM,IAAI,UAAU,GAAG,GAAG,SAAS,GAAG;AACpE,YAAM,IAAI,UAAU,KAAK,SAAS;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,WAAsB,OAA2C;AAC5F,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,mBAAmB;AAAA,MACjB,YAAY,MAAM;AAAA,MAClB,WAAW,CAAC,MAAM,cAAc;AAAA,MAChC,YAAY,CAAC,MAAM,cAAc;AAAA,MACjC,UAAU,MAAM;AAAA,IAClB;AAAA,IACA,wBAAwB,MAAM;AAAA,IAC9B,iBAAiB,CAAC,MAAM,cAAc;AAAA,EACxC;AACF;AAEO,SAAS,aAAa,UAAkB,QAAwB;AACrE,SAAO,KAAK,UAAU;AAAA,IACpB;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAEO,SAAS,YAAY,OAA0D;AACpF,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAI,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,KAAK,EAAE,WAAW,EAAG,QAAO;AACvF,QAAI,OAAO,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,OAAO,MAAM,KAAK,OAAO,SAAS,EAAG,QAAO;AACtG,WAAO;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,QAAQ,OAAO;AAAA,IACjB;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,KAAoB,QAAyC;AACjF,QAAM,SAA4B,CAAC;AACnC,QAAM,eAAwC,CAAC;AAC/C,QAAM,gBAAoC,CAAC;AAE3C,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,gBAAgB,SAAU;AACpC,UAAM,WAAW,IAAI,MAAM,aAAa;AACxC,QAAI,aAAa,UAAa,aAAa,KAAM;AACjD,QAAI,MAAM,gBAAgB,kBAAkB,MAAM,WAAW,WAAW,KAAK,GAAG;AAC9E,YAAM,iBAAiB,MAAM,WAAW,WAAW,KAAK,IAAI,MAAM,WAAW,MAAM,CAAC,IAAI,MAAM;AAC9F,UAAI,eAAe,KAAK,EAAE,SAAS,GAAG;AACpC,qBAAa,cAAc,IAAI;AAAA,MACjC;AACA;AAAA,IACF;AAEA,QAAI,MAAM,eAAe,qBAAqB;AAC5C,aAAO,aAAa,wBAAwB,QAAQ;AACpD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,oBAAoB;AAC3C,aAAO,YAAY,wBAAwB,QAAQ;AACnD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,mBAAmB;AAC1C,aAAO,WAAW,wBAAwB,QAAQ;AAClD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,sBAAsB;AAC7C,aAAO,cAAc,wBAAwB,QAAQ;AACrD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,uBAAuB;AAC9C,aAAO,eAAe,eAAe,QAAQ;AAC7C;AAAA,IACF;AACA,QAAI,MAAM,eAAe,uBAAuB;AAC9C,aAAO,eAAe,uBAAuB,UAAU,GAAG;AAC1D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,mBAAmB;AAC1C,aAAO,WAAW,wBAAwB,QAAQ;AAClD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,iBAAiB;AACxC,aAAO,SAAS,wBAAwB,QAAQ;AAChD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,iBAAiB;AACxC,aAAO,SAAS,wBAAwB,QAAQ;AAChD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,sBAAsB;AAC7C,aAAO,cAAc,wBAAwB,QAAQ;AACrD;AAAA,IACF;AAEA,QAAI,MAAM,eAAe,gBAAgB;AACvC,oBAAc,OAAO,wBAAwB,QAAQ;AACrD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,mBAAmB;AAC1C,oBAAc,UAAU,wBAAwB,QAAQ;AACxD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,uBAAuB;AAC9C,oBAAc,cAAc,wBAAwB,QAAQ;AAC5D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,wBAAwB;AAC/C,oBAAc,eAAe,wBAAwB,QAAQ;AAC7D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,wBAAwB;AAC/C,oBAAc,eAAe,wBAAwB,QAAQ;AAC7D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,0BAA0B;AACjD,oBAAc,iBAAiB,wBAAwB,QAAQ;AAC/D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,sBAAsB;AAC7C,oBAAc,aAAa,wBAAwB,QAAQ;AAC3D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,gBAAgB;AACvC,oBAAc,OAAO,wBAAwB,QAAQ;AACrD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,kBAAkB;AACzC,oBAAc,SAAS,wBAAwB,QAAQ;AACvD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,sBAAsB;AAC7C,oBAAc,aAAa,wBAAwB,QAAQ;AAC3D;AAAA,IACF;AACA,QAAI,MAAM,eAAe,mBAAmB;AAC1C,oBAAc,UAAU,wBAAwB,QAAQ;AACxD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,oBAAoB;AAC3C,oBAAc,WAAW,wBAAwB,QAAQ;AACzD;AAAA,IACF;AACA,QAAI,MAAM,eAAe,qBAAqB;AAC5C,oBAAc,YAAY,wBAAwB,QAAQ;AAAA,IAC5D;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,QAAgG;AACzH,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,sBAAsB,OAAO,eAAe;AAElD,MAAI,aAAa,UAAU;AACzB,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,aAAa,uBAAuB,GAAG,SAAS,IAAI,QAAQ,GAAG,KAAK;AAAA,IACtE;AAAA,EACF;AAEA,MAAI,qBAAqB;AACvB,UAAM,QAAQ,oBAAoB,MAAM,KAAK,EAAE,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AAC/E,QAAI,MAAM,UAAU,GAAG;AACrB,aAAO;AAAA,QACL,WAAW,aAAa,MAAM,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG;AAAA,QACnD,UAAU,YAAY,MAAM,GAAG,EAAE,KAAK;AAAA,QACtC,aAAa;AAAA,MACf;AAAA,IACF;AACA,WAAO;AAAA,MACL,WAAW,aAAa;AAAA,MACxB,UAAU,YAAY;AAAA,MACtB,aAAa;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,mBAAmB,KAAoB,SAAsB,OAAwC;AACnH,QAAM,EAAE,QAAQ,cAAc,cAAc,IAAI,aAAa,KAAK,QAAQ,MAAM;AAChF,QAAM,eAAe,kBAAkB,MAAM;AAC7C,QAAM,mBAAmB,OAAO,cAAc,OAAO,gBAAgB,OAAO,eAAe;AAE3F,QAAM,cAAiD;AAAA,IACrD,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,EAClB;AAEA,MAAI,OAAO,aAAc,aAAY,eAAe,OAAO;AAC3D,MAAI,OAAO,aAAc,aAAY,eAAe,OAAO;AAC3D,MAAI,OAAO,SAAU,aAAY,WAAW,OAAO;AACnD,MAAI,OAAO,OAAQ,aAAY,SAAS,OAAO;AAC/C,MAAI,OAAO,OAAQ,aAAY,SAAS,OAAO;AAC/C,MAAI,OAAO,YAAa,aAAY,cAAc,OAAO;AACzD,MAAI,cAAc,UAAW,aAAY,YAAY,aAAa;AAClE,MAAI,cAAc,SAAU,aAAY,WAAW,aAAa;AAChE,MAAI,cAAc,YAAa,aAAY,cAAc,aAAa;AAEtE,MAAI,CAAC,cAAc;AACjB,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,MACX,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,WAAW,aAAa;AAAA,MACxB,UAAU,aAAa;AAAA,MACvB,aAAa,aAAa;AAAA,MAC1B,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,MACnE,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,MACnE,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;AAAA,MACvD,GAAI,OAAO,SAAS,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjD,GAAI,OAAO,SAAS,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjD,GAAI,OAAO,cAAc,EAAE,aAAa,OAAO,YAAY,IAAI,CAAC;AAAA,IAClE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,iBAAiB,eAA4C;AACpE,SAAO,OAAO,OAAO,aAAa,EAAE,KAAK,CAAC,UAAU,UAAU,QAAQ,UAAU,MAAS;AAC3F;AAEA,SAAS,wBAAwB,eAAmC,UAAkB,OAAoB;AACxG,MAAI,CAAC,cAAc,aAAc,QAAO;AAExC,SAAO;AAAA,IACL,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB;AAAA,IACA,cAAc,cAAc;AAAA,IAC5B,WAAW;AAAA,IACX,GAAI,cAAc,OAAO,EAAE,MAAM,cAAc,KAAK,IAAI,CAAC;AAAA,IACzD,GAAI,cAAc,UAAU,EAAE,SAAS,cAAc,QAAQ,IAAI,CAAC;AAAA,IAClE,GAAI,cAAc,cAAc,EAAE,aAAa,cAAc,YAAY,IAAI,CAAC;AAAA,IAC9E,GAAI,cAAc,eAAe,EAAE,cAAc,cAAc,aAAa,IAAI,CAAC;AAAA,IACjF,GAAI,cAAc,iBAAiB,EAAE,gBAAgB,cAAc,eAAe,IAAI,CAAC;AAAA,IACvF,GAAI,cAAc,aAAa,EAAE,YAAY,cAAc,WAAW,IAAI,CAAC;AAAA,IAC3E,GAAI,cAAc,OAAO,EAAE,MAAM,cAAc,KAAK,IAAI,CAAC;AAAA,IACzD,GAAI,cAAc,SAAS,EAAE,QAAQ,cAAc,OAAO,IAAI,CAAC;AAAA,IAC/D,GAAI,cAAc,aAAa,EAAE,YAAY,cAAc,WAAW,IAAI,CAAC;AAAA,IAC3E,GAAI,cAAc,UAAU,EAAE,SAAS,cAAc,QAAQ,IAAI,CAAC;AAAA,IAClE,GAAI,cAAc,aAAa,QAAQ,cAAc,aAAa,SAAY,EAAE,UAAU,cAAc,SAAS,IAAI,CAAC;AAAA,IACtH,GAAI,cAAc,cAAc,QAAQ,cAAc,cAAc,SAAY,EAAE,WAAW,cAAc,UAAU,IAAI,CAAC;AAAA,EAC5H;AACF;AAEA,eAAe,qBAAqB,QAOlB;AAChB,MAAI,CAAC,iBAAiB,OAAO,aAAa,EAAG;AAC7C,MAAI,CAAC,OAAO,cAAc,aAAc;AAExC,QAAM,CAAC,sBAAsB,IAAI,MAAM;AAAA,IACrC,OAAO;AAAA,IACP;AAAA,IACA;AAAA,MACE,QAAQ,OAAO;AAAA,MACf,gBAAgB,OAAO,MAAM;AAAA,MAC7B,UAAU,OAAO,MAAM;AAAA,MACvB,WAAW;AAAA,IACb;AAAA,IACA;AAAA,MACE,SAAS;AAAA,QACP,WAAW;AAAA,MACb;AAAA,MACA,OAAO;AAAA,IACT;AAAA,IACA,OAAO;AAAA,EACT;AAEA,MAAI,wBAAwB;AAC1B,UAAM,OAAO,WAAW,QAAQ,8BAA8B;AAAA,MAC5D,OAAO;AAAA,QACL,IAAI,uBAAuB;AAAA,QAC3B,WAAW;AAAA,QACX,GAAI,OAAO,cAAc,OAAO,EAAE,MAAM,OAAO,cAAc,KAAK,IAAI,CAAC;AAAA,QACvE,GAAI,OAAO,cAAc,UAAU,EAAE,SAAS,OAAO,cAAc,QAAQ,IAAI,CAAC;AAAA,QAChF,GAAI,OAAO,cAAc,cAAc,EAAE,aAAa,OAAO,cAAc,YAAY,IAAI,CAAC;AAAA,QAC5F,GAAI,OAAO,cAAc,eAAe,EAAE,cAAc,OAAO,cAAc,aAAa,IAAI,CAAC;AAAA,QAC/F,GAAI,OAAO,cAAc,eAAe,EAAE,cAAc,OAAO,cAAc,aAAa,IAAI,CAAC;AAAA,QAC/F,GAAI,OAAO,cAAc,iBAAiB,EAAE,gBAAgB,OAAO,cAAc,eAAe,IAAI,CAAC;AAAA,QACrG,GAAI,OAAO,cAAc,aAAa,EAAE,YAAY,OAAO,cAAc,WAAW,IAAI,CAAC;AAAA,QACzF,GAAI,OAAO,cAAc,OAAO,EAAE,MAAM,OAAO,cAAc,KAAK,IAAI,CAAC;AAAA,QACvE,GAAI,OAAO,cAAc,SAAS,EAAE,QAAQ,OAAO,cAAc,OAAO,IAAI,CAAC;AAAA,QAC7E,GAAI,OAAO,cAAc,aAAa,EAAE,YAAY,OAAO,cAAc,WAAW,IAAI,CAAC;AAAA,QACzF,GAAI,OAAO,cAAc,UAAU,EAAE,SAAS,OAAO,cAAc,QAAQ,IAAI,CAAC;AAAA,QAChF,GAAI,OAAO,cAAc,aAAa,QAAQ,OAAO,cAAc,aAAa,SAC5E,EAAE,UAAU,OAAO,cAAc,SAAS,IAC1C,CAAC;AAAA,QACL,GAAI,OAAO,cAAc,cAAc,QAAQ,OAAO,cAAc,cAAc,SAC9E,EAAE,WAAW,OAAO,cAAc,UAAU,IAC5C,CAAC;AAAA,MACP;AAAA,MACA,KAAK,OAAO;AAAA,IACd,CAAC;AACD;AAAA,EACF;AAEA,QAAM,cAAc,wBAAwB,OAAO,eAAe,OAAO,UAAU,OAAO,KAAK;AAC/F,MAAI,CAAC,YAAa;AAElB,QAAM,OAAO,WAAW,QAAQ,8BAA8B;AAAA,IAC5D,OAAO;AAAA,IACP,KAAK,OAAO;AAAA,EACd,CAAC;AACH;AAEA,eAAe,kBAAkB,IAAmB,YAAoB,OAA0C;AAChH,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,MACE,eAAe;AAAA,MACf;AAAA,MACA,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,QAAQ,WAAW,OAAO,OAAO,YAAY,UAAU;AACzD,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,eAAe;AAAA,EACjB;AACF;AAEA,eAAe,cAAc,IAAmB,OAA2B,QAAgC,OAAqD;AAC9J,MAAI,OAAO;AACT,UAAM,cAAc,MAAM;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,QACE,WAAW;AAAA,QACX,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,YAAa,QAAO;AAAA,EAC1B;AAEA,MAAI,QAAQ,UAAU;AACpB,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI,OAAO;AAAA,QACX,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAe,wBAAwB,QAMZ;AACzB,MAAI,OAAO,YAAY;AACrB,UAAM,gBAAgB,MAAM,OAAO,yBAAyB;AAAA,MAC1D;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,IACT;AACA,QAAI,cAAe,QAAO;AAAA,EAC5B;AAEA,MAAI,CAAC,OAAO,MAAO,QAAO;AAC1B,SAAO,OAAO,iBAAiB,IAAI,OAAO,KAAK,KAAK;AACtD;AAEA,SAAS,qBAAqB,SAA+B;AAC3D,SAAO,QAAQ,OAAO,KAAK,CAAC,UAAU,MAAM,eAAe,qBAAqB;AAClF;AAEA,eAAe,sBAAsB,QAIP;AAC5B,MAAI,CAAC,qBAAqB,OAAO,OAAO,EAAG,QAAO,oBAAI,IAAI;AAE1D,QAAM,aAAa,MAAM;AAAA,IACvB,OAAO;AAAA,IACP;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,gBAAgB,OAAO,MAAM;AAAA,MAC7B,UAAU,OAAO,MAAM;AAAA,MACvB,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,SAAS;AAAA,QACP,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,OAAO;AAAA,EACT;AAEA,QAAM,QAA0B,oBAAI,IAAI;AACxC,aAAW,aAAa,YAAY;AAClC,UAAM,QAAQ,eAAe,UAAU,YAAY;AACnD,QAAI,CAAC,SAAS,MAAM,IAAI,KAAK,EAAG;AAChC,UAAM,IAAI,OAAO,UAAU,EAAE;AAAA,EAC/B;AACA,SAAO;AACT;AAEA,SAAS,WAAW,KAA6B;AAC/C,SAAO,CAAC,OAAO,OAAO,GAAG,EAAE,KAAK,CAAC,UAAU,wBAAwB,KAAK,CAAC;AAC3E;AAEA,eAAe,WAAW,QAWF;AACtB,MAAI,WAAW,OAAO,GAAG,GAAG;AAC1B,WAAO;AAAA,MACL,YAAY,OAAO,OAAO,SAAS;AAAA,MACnC,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,WAAW,OAAO;AAAA,QAClB,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,mBAAmB,OAAO,KAAK,OAAO,SAAS,OAAO,KAAK;AAC3E,QAAM,aAAa,QAAQ,OAAO,cAAc;AAChD,QAAM,mBAAmB,QAAQ,oBAAoB,OAAO,OAAO,SAAS;AAC5E,QAAM,oBAAoB,mBAAmB,QAAQ,cAAc,OAAO,sBAAsB;AAChG,MAAI,CAAC,kBAAkB,IAAI;AACzB,WAAO;AAAA,MACL,YAAY,cAAc;AAAA,MAC1B,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,WAAW,OAAO;AAAA,QAClB;AAAA,QACA,cAAc,kBAAkB;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AACA,QAAM,aAAa,MAAM,wBAAwB;AAAA,IAC/C,0BAA0B,OAAO;AAAA,IACjC;AAAA,IACA,OAAO,QAAQ,OAAO;AAAA,IACtB,kBAAkB,OAAO;AAAA,IACzB,OAAO,OAAO;AAAA,EAChB,CAAC;AAED,MAAI,CAAC,cAAc,CAAC,QAAQ,aAAa;AACvC,WAAO;AAAA,MACL,YAAY,cAAc;AAAA,MAC1B,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,WAAW,OAAO;AAAA,QAClB;AAAA,QACA,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACF,QAAI,YAAY;AACd,YAAM,cAAc;AAAA,QAClB,IAAI;AAAA,QACJ,GAAG,QAAQ;AAAA,QACX,GAAI,OAAO,KAAK,kBAAkB,MAAM,EAAE,SAAS,IAAI,EAAE,cAAc,kBAAkB,OAAO,IAAI,CAAC;AAAA,MACvG;AACA,YAAM,OAAO,WAAW,QAAQ,2BAA2B;AAAA,QACzD,OAAO;AAAA,QACP,KAAK,OAAO;AAAA,MACd,CAAC;AAED,YAAM,qBAAqB;AAAA,QACzB,UAAU;AAAA,QACV,eAAe,QAAQ;AAAA,QACvB,OAAO,OAAO;AAAA,QACd,YAAY,OAAO;AAAA,QACnB,gBAAgB,OAAO;AAAA,QACvB,IAAI,OAAO;AAAA,MACb,CAAC;AAED,UAAI,YAAY;AACd,cAAM,OAAO,yBAAyB;AAAA,UACpC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO;AAAA,QACT;AAAA,MACF;AACA,UAAI,QAAQ,OAAO,gBAAgB,CAAC,OAAO,iBAAiB,IAAI,QAAQ,OAAO,YAAY,GAAG;AAC5F,eAAO,iBAAiB,IAAI,QAAQ,OAAO,cAAc,UAAU;AAAA,MACrE;AAEA,aAAO;AAAA,QACL,YAAY,cAAc;AAAA,QAC1B,QAAQ;AAAA,QACR,MAAM;AAAA,UACJ,SAAS;AAAA,UACT,WAAW,OAAO;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,OAAO,WAAW,QAG5C,2BAA2B;AAAA,MAC3B,OAAO;AAAA,QACL,GAAG,QAAQ;AAAA,QACX,GAAI,OAAO,KAAK,kBAAkB,MAAM,EAAE,SAAS,IAAI,EAAE,cAAc,kBAAkB,OAAO,IAAI,CAAC;AAAA,MACvG;AAAA,MACA,KAAK,OAAO;AAAA,IACd,CAAC;AAED,UAAM,qBAAqB;AAAA,MACzB,UAAU,cAAc,OAAO;AAAA,MAC/B,eAAe,QAAQ;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,gBAAgB,OAAO;AAAA,MACvB,IAAI,OAAO;AAAA,IACb,CAAC;AAED,QAAI,YAAY;AACd,YAAM,OAAO,yBAAyB;AAAA,QACpC;AAAA,QACA;AAAA,QACA,cAAc,OAAO;AAAA,QACrB;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF;AACA,QAAI,QAAQ,OAAO,gBAAgB,CAAC,OAAO,iBAAiB,IAAI,QAAQ,OAAO,YAAY,GAAG;AAC5F,aAAO,iBAAiB,IAAI,QAAQ,OAAO,cAAc,cAAc,OAAO,QAAQ;AAAA,IACxF;AAEA,WAAO;AAAA,MACL,YAAY,cAAc;AAAA,MAC1B,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,SAAS,cAAc,OAAO;AAAA,QAC9B,UAAU,cAAc,OAAO;AAAA,QAC/B,WAAW,OAAO;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,UAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU;AAC9D,WAAO;AAAA,MACL,YAAY,cAAc;AAAA,MAC1B,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,WAAW,OAAO;AAAA,QAClB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,4BAA6C;AAAA,EACxD,aAAa;AAAA,EACb,SAAS;AAAA,EACT,sBAAsB;AAAA,EACtB,WAAW;AAAA,EACX,mBAAmB,CAAC,kBAAkB;AAAA,EAEtC,MAAM,WAAW,OAA6B;AAC5C,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,WAAO,kBAAkB,IAAI,MAAM,YAAY,MAAM,KAAK;AAAA,EAC5D;AAAA,EAEA,OAAO,aAAa,OAAmC;AACrD,QAAI,MAAM,eAAe,oBAAoB;AAC3C,YAAM,IAAI,MAAM,uCAAuC,MAAM,UAAU,EAAE;AAAA,IAC3E;AAEA,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,UAAM,aAAa,UAAU,QAAQ,YAAY;AACjD,UAAM,2BAA2B,UAAU,QAAQ,0BAA0B;AAC7E,UAAM,SAAS,YAAY,MAAM,MAAM;AACvC,UAAM,SAAS,MAAM,cAAc,IAAI,MAAM,OAAO,QAAQ,MAAM,KAAK;AAEvE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,qDAAqD;AAAA,IACvE;AAEA,QAAI,MAAM,OAAO;AACf,aAAO,YAAY,MAAM;AAAA,IAC3B;AACA,WAAO,SAAS;AAChB,UAAM,GAAG,MAAM;AAEf,QAAI;AACF,YAAM,aAAa,MAAM;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,UACE,IAAI,OAAO;AAAA,UACX,gBAAgB,MAAM,MAAM;AAAA,UAC5B,UAAU,MAAM,MAAM;AAAA,QACxB;AAAA,QACA;AAAA,QACA,MAAM;AAAA,MACR;AAEA,UAAI,CAAC,YAAY;AACf,cAAM,IAAI,MAAM,2CAA2C;AAAA,MAC7D;AAEA,YAAM,WAAW,MAAM,uBAAuB,MAAM,gCAAgC,UAAU,CAAC;AAC/F,YAAM,cAAc,QAAQ,aAAa,OAAO,KAAK,OAAO,SAAS;AACrE,YAAM,iBAAiB,oBAAoB,WAAW,MAAM,KAAK;AACjE,YAAM,yBAAyB,MAAM,iCAAiC,IAAI,MAAM,KAAK;AACrF,YAAM,mBAAmB,MAAM,sBAAsB;AAAA,QACnD;AAAA,QACA,SAAS,MAAM;AAAA,QACf,OAAO,MAAM;AAAA,MACf,CAAC;AACD,UAAI,aAAa;AAEjB,uBAAiB,SAAS,wBAAwB,MAAM,gCAAgC,UAAU,GAAG;AAAA,QACnG,WAAW,MAAM;AAAA,QACjB;AAAA,MACF,CAAC,GAAG;AACF,cAAM,YAAY,MAAM;AACxB,cAAM,QAAsB,CAAC;AAE7B,iBAAS,QAAQ,GAAG,QAAQ,UAAU,QAAQ,SAAS,GAAG;AAKxD,cAAI,MAAM,QAAQ,QAAS;AAC3B,gBAAM,KAAK,MAAM,WAAW;AAAA,YAC1B,KAAK,UAAU,KAAK;AAAA,YACpB,WAAW,MAAM,WAAW,QAAQ;AAAA,YACpC,SAAS,MAAM;AAAA,YACf,OAAO,MAAM;AAAA,YACb;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF,CAAC,CAAC;AAAA,QACJ;AAEA,cAAM;AAAA,UACJ;AAAA,UACA,QAAQ,aAAa,OAAO,IAAI,MAAM,UAAU;AAAA,UAChD,SAAS,MAAM,aAAa,SAAS;AAAA,UACrC,eAAe,SAAS;AAAA,UACxB,gBAAgB,UAAU;AAAA,UAC1B,4BAA4B;AAAA,UAC5B;AAAA,UACA,SAAS,aAAa,MAAM,UAAU,OAAO,SAAS,SAAS;AAAA,QACjE;AACA,sBAAc;AAAA,MAChB;AAEA,aAAO,SAAS;AAChB,YAAM,GAAG,MAAM;AAAA,IACjB,SAAS,OAAO;AACd,aAAO,SAAS;AAChB,YAAM,GAAG,MAAM;AACf,YAAM;AAAA,IACR;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7029.1.a1bb3363af",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7029.1.a1bb3363af",
|
|
256
|
+
"@open-mercato/shared": "0.6.8-develop.7029.1.a1bb3363af",
|
|
257
|
+
"@open-mercato/ui": "0.6.8-develop.7029.1.a1bb3363af",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
263
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
264
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7029.1.a1bb3363af",
|
|
263
|
+
"@open-mercato/shared": "0.6.8-develop.7029.1.a1bb3363af",
|
|
264
|
+
"@open-mercato/ui": "0.6.8-develop.7029.1.a1bb3363af",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
|
@@ -206,7 +206,7 @@ If the sync provider needs bootstrap credentials, mappings, locales, channels, o
|
|
|
206
206
|
- **Resetting an opt-out**: A reset flow that deletes the shared `SyncCursor` row MUST also call `syncRunService.resetResumePosition(integrationId, entityType, direction, scope)`. An opted-out entity type has no shared row to delete, so deleting only that leaves the resume position on the last interrupted run and the next incremental run re-imports just the tail of the walk it was reset against. The call is a no-op when nothing is interrupted, so make it unconditionally
|
|
207
207
|
- **Resume**: Retry reads the last successful cursor, resumes from there
|
|
208
208
|
- **Progress**: Linked to `ProgressJob` via `progressJobId` for `ProgressTopBar` display
|
|
209
|
-
- **Cancellation**:
|
|
209
|
+
- **Cancellation**: The engine polls `progressService.isCancellationRequested()` in the batch handler AND on the heartbeat tick while a batch is still in flight, aborting `StreamImportInput.signal` / `StreamExportInput.signal`. Adapters SHOULD honour the signal wherever the work is divisible (per page, per record, around a long flush) and `return` — with the `return` ABOVE the `yield`, never below it, or the engine commits a cursor for a half-applied page. Adapters that ignore the signal keep the old between-batches behavior.
|
|
210
210
|
- **Tracing**: The engine emits one **root** span per batch (`data_sync.import.batch` / `data_sync.export.batch`) linked back to the run, covering the adapter's read *and* the engine's bookkeeping. Adapters MUST NOT hand-roll their own batch span — they cannot root it, so a multi-day run would ride on the single sampling decision taken for the request that triggered it. Inner spans an adapter creates nest under the batch span normally. The final read — the one that finds the stream drained — is traced as `data_sync.import.drain` / `data_sync.export.drain`, so N batches emit exactly N `*.batch` spans plus one `*.drain`.
|
|
211
211
|
- **Stream shape**: The engine drives the adapter's async iterator explicitly (`batch-stream.ts`) so the span wraps `next()`, where a generator does its real work before yielding. Closing follows the language's own `IteratorClose` rules, so `finally` blocks in an adapter generator behave exactly as under `for await`: no `return()` when the stream exhausts or `next()` throws (already closed), `return()` with its failure surfaced on an early stop, and `return()` with its failure swallowed when the engine's own handler threw (that error wins). Keep cleanup in `finally`.
|
|
212
212
|
|
|
@@ -45,6 +45,27 @@ export interface StreamImportInput {
|
|
|
45
45
|
* declares no parameters.
|
|
46
46
|
*/
|
|
47
47
|
parameters?: Record<string, RunParameterValue>
|
|
48
|
+
/**
|
|
49
|
+
* Aborted when the run is cancelled, so an adapter can stop INSIDE a batch.
|
|
50
|
+
*
|
|
51
|
+
* The engine only gets to look at cancellation between batches — its check sits in the batch
|
|
52
|
+
* handler, which runs after the adapter has yielded. An adapter whose batch takes minutes (a
|
|
53
|
+
* whole-table walk over a slow link) therefore keeps working, keeps writing, and keeps its
|
|
54
|
+
* advisory lock for that whole time, however long ago the operator pressed Cancel.
|
|
55
|
+
*
|
|
56
|
+
* Honour it wherever the work is divisible — per page, per record, around a long flush — and just
|
|
57
|
+
* return: the generator's own `finally` runs, which is where a lock or a connection is released.
|
|
58
|
+
* Adapters that ignore it behave exactly as they do today.
|
|
59
|
+
*
|
|
60
|
+
* The `return` MUST sit ABOVE the `yield` for the page you abandoned, never below it. The engine
|
|
61
|
+
* commits `batch.cursor` for every batch it receives, so yielding a half-applied page advances the
|
|
62
|
+
* cursor past records that were never applied and no later run ever walks them again.
|
|
63
|
+
*
|
|
64
|
+
* The signal only reaches work running in THIS process. An adapter that hands part of a batch to
|
|
65
|
+
* other workers must give that work its own cancellation check against the same progress job —
|
|
66
|
+
* aborting here stops the generator, not anything already queued elsewhere.
|
|
67
|
+
*/
|
|
68
|
+
signal?: AbortSignal
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export interface ImportItem {
|
|
@@ -57,6 +78,18 @@ export interface ImportItem {
|
|
|
57
78
|
export interface ImportBatch {
|
|
58
79
|
items: ImportItem[]
|
|
59
80
|
cursor: string
|
|
81
|
+
/**
|
|
82
|
+
* Whether the source has more to give after this batch. The final batch MUST report `false`.
|
|
83
|
+
*
|
|
84
|
+
* This is not advisory. The engine uses the last batch's value to tell a stream that DRAINED from
|
|
85
|
+
* one the adapter STOPPED EARLY on {@link StreamImportInput.signal}, because both end the same way
|
|
86
|
+
* — the generator simply returns. An adapter that reports `true` on its final batch will have a
|
|
87
|
+
* complete run misreported as `cancelled` whenever a cancel lands during the final read: the
|
|
88
|
+
* operator is told a finished sync was partial, and the run stays resumable with nothing left to
|
|
89
|
+
* resume.
|
|
90
|
+
*
|
|
91
|
+
* Derive it from the source rather than hardcoding it — `Boolean(nextPage)`, `offset < total`.
|
|
92
|
+
*/
|
|
60
93
|
hasMore: boolean
|
|
61
94
|
totalEstimate?: number
|
|
62
95
|
processedCount?: number
|
|
@@ -76,6 +109,8 @@ export interface StreamExportInput {
|
|
|
76
109
|
runId?: string
|
|
77
110
|
/** See {@link StreamImportInput.parameters}. */
|
|
78
111
|
parameters?: Record<string, RunParameterValue>
|
|
112
|
+
/** Aborted when the run is cancelled — see {@link StreamImportInput.signal}. */
|
|
113
|
+
signal?: AbortSignal
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
export interface ExportItemResult {
|
|
@@ -88,6 +123,7 @@ export interface ExportItemResult {
|
|
|
88
123
|
export interface ExportBatch {
|
|
89
124
|
results: ExportItemResult[]
|
|
90
125
|
cursor: string
|
|
126
|
+
/** Whether the source has more to give; the final batch MUST report `false`. See {@link ImportBatch.hasMore}. */
|
|
91
127
|
hasMore: boolean
|
|
92
128
|
batchIndex: number
|
|
93
129
|
}
|
|
@@ -94,12 +94,28 @@ function applyExportCounters(batch: ExportBatch): SyncCounterDelta {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Adapter batches can legitimately outlast the stale-job sweep window (slow upstream
|
|
97
|
-
// APIs), so the engine must heartbeat while a batch is still being produced.
|
|
97
|
+
// APIs), so the engine must heartbeat while a batch is still being produced. The same
|
|
98
|
+
// tick also polls cancellation, so a cancel lands within one interval instead of waiting
|
|
99
|
+
// out the batch — sharing this timer rather than adding a second one that would double
|
|
100
|
+
// the per-interval round-trips for the whole life of a run.
|
|
98
101
|
const HEARTBEAT_TICK_MS = (STALE_JOB_TIMEOUT_SECONDS * 1000) / 4
|
|
99
102
|
|
|
100
103
|
// Runs `tick` on an interval only while the source iterator is pending, so heartbeats
|
|
101
104
|
// stop the moment the producer dies and genuinely stale jobs still get swept. The outer
|
|
102
105
|
// finally closes the adapter generator on early exits (cancellation, ownership conflict).
|
|
106
|
+
// Our own abort, as opposed to a failure that merely coincided with one. Adapters are told to
|
|
107
|
+
// return rather than throw, but `signal.throwIfAborted()` and an aborted `fetch` both surface as
|
|
108
|
+
// this, and either is a cancellation rather than a fault.
|
|
109
|
+
//
|
|
110
|
+
// Matched structurally on `name` rather than with `instanceof Error`, because those two throw a
|
|
111
|
+
// `DOMException`, and whether that inherits from `Error` depends on the runtime — it does under
|
|
112
|
+
// bare Node 24 and does NOT under the jest environment this is tested in. An `instanceof` test
|
|
113
|
+
// therefore passes or fails on where the code runs, which is not something cancellation should
|
|
114
|
+
// depend on.
|
|
115
|
+
function isAbortError(error: unknown): boolean {
|
|
116
|
+
return typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'AbortError'
|
|
117
|
+
}
|
|
118
|
+
|
|
103
119
|
async function* withHeartbeat<T>(source: AsyncIterable<T>, tick: () => void, intervalMs: number): AsyncGenerator<T, void, undefined> {
|
|
104
120
|
const iterator = source[Symbol.asyncIterator]()
|
|
105
121
|
try {
|
|
@@ -187,6 +203,32 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
187
203
|
}
|
|
188
204
|
}
|
|
189
205
|
|
|
206
|
+
// Rides the heartbeat timer, which is the only thing that runs while the adapter is still
|
|
207
|
+
// producing a batch — the engine's own cancellation check sits in the batch handler and is
|
|
208
|
+
// reached only after a yield. Swallows its own errors because it runs on a timer, where an
|
|
209
|
+
// unhandled rejection is fatal, and stops polling once it has aborted.
|
|
210
|
+
function makeCancellationTick(progressJobId: string | null | undefined, scope: SyncScope, controller: AbortController): () => void {
|
|
211
|
+
if (!progressJobId) return () => {}
|
|
212
|
+
let inFlight = false
|
|
213
|
+
return () => {
|
|
214
|
+
if (inFlight || controller.signal.aborted) return
|
|
215
|
+
inFlight = true
|
|
216
|
+
progressService.isCancellationRequested(progressJobId, scope.tenantId, scope.organizationId)
|
|
217
|
+
.then((cancelled) => {
|
|
218
|
+
if (cancelled) controller.abort()
|
|
219
|
+
})
|
|
220
|
+
.catch((error) => {
|
|
221
|
+
logger.warn('Cancellation poll failed', {
|
|
222
|
+
progressJobId,
|
|
223
|
+
error: error instanceof Error ? error.message : String(error),
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
.finally(() => {
|
|
227
|
+
inFlight = false
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
async function refreshCoverageSnapshots(entityTypes: string[] | undefined, scope: SyncScope): Promise<void> {
|
|
191
233
|
if (!entityTypes || entityTypes.length === 0) return
|
|
192
234
|
|
|
@@ -559,10 +601,17 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
559
601
|
let processedCount = await seedProcessedCount(run.progressJobId, scope)
|
|
560
602
|
let totalCount: number | null = null
|
|
561
603
|
let committedBatches = activeRun.batchesCompleted ?? 0
|
|
604
|
+
// Whether the last committed batch said the source was exhausted. Distinguishes a stream that
|
|
605
|
+
// drained from one the adapter stopped early — see the post-stream finalize below.
|
|
606
|
+
let streamReportedDone = false
|
|
562
607
|
// Captured while the triggering job's span is still the active one, so
|
|
563
608
|
// every rooted batch trace can link back to it.
|
|
564
609
|
const runTrace = captureTelemetryTrace()
|
|
565
610
|
const spanAttributes = runSpanAttributes(run, providerKey, scope)
|
|
611
|
+
// Declared outside the try because both the catch and the completion path below read it.
|
|
612
|
+
const cancellation = new AbortController()
|
|
613
|
+
const heartbeat = makeHeartbeatTick(run.progressJobId, scope)
|
|
614
|
+
const pollCancellation = makeCancellationTick(run.progressJobId, scope, cancellation)
|
|
566
615
|
|
|
567
616
|
try {
|
|
568
617
|
const streamResult = await forEachBatch(
|
|
@@ -576,8 +625,11 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
576
625
|
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
577
626
|
runId: run.id,
|
|
578
627
|
parameters: (run.parameters ?? {}) as RunParameters,
|
|
628
|
+
signal: cancellation.signal,
|
|
579
629
|
}),
|
|
580
|
-
|
|
630
|
+
// `finally` so a synchronous throw from the heartbeat cannot also stop cancellation
|
|
631
|
+
// from being observed for the rest of the run.
|
|
632
|
+
() => { try { heartbeat() } finally { pollCancellation() } },
|
|
581
633
|
HEARTBEAT_TICK_MS,
|
|
582
634
|
),
|
|
583
635
|
{
|
|
@@ -592,7 +644,7 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
592
644
|
'data_sync.batch_size': batch.items.length,
|
|
593
645
|
})
|
|
594
646
|
|
|
595
|
-
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
647
|
+
if (cancellation.signal.aborted || (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId))) {
|
|
596
648
|
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
597
649
|
return 'stop'
|
|
598
650
|
}
|
|
@@ -621,6 +673,7 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
621
673
|
{ expectedBatchesCompleted: committedBatches, persistSharedCursor },
|
|
622
674
|
)
|
|
623
675
|
committedBatches += 1
|
|
676
|
+
streamReportedDone = batch.hasMore === false
|
|
624
677
|
|
|
625
678
|
await updateProgress(run.progressJobId, processedCount, totalCount, scope)
|
|
626
679
|
await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope)
|
|
@@ -657,6 +710,15 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
657
710
|
})
|
|
658
711
|
return
|
|
659
712
|
}
|
|
713
|
+
// An adapter that honours the signal may reject instead of returning, so our own abort is
|
|
714
|
+
// a cancellation rather than a fault — and must not leave an `error` entry in the
|
|
715
|
+
// integration log for a run the operator cancelled on purpose. Anything else that merely
|
|
716
|
+
// coincided with the cancel — a rejecting commit, an upstream 500 — is a genuine failure
|
|
717
|
+
// and keeps its log entry, its message, its `failed` status and its failed event.
|
|
718
|
+
if (cancellation.signal.aborted && isAbortError(error)) {
|
|
719
|
+
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
720
|
+
return
|
|
721
|
+
}
|
|
660
722
|
const message = error instanceof Error ? error.message : 'Sync import failed'
|
|
661
723
|
await integrationLogService.write(
|
|
662
724
|
{
|
|
@@ -671,6 +733,19 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
671
733
|
return
|
|
672
734
|
}
|
|
673
735
|
|
|
736
|
+
// An adapter that honours the signal stops mid-batch and returns WITHOUT yielding, so the
|
|
737
|
+
// batch handler — which owns the only other `cancelled` transition — never runs and the
|
|
738
|
+
// stream reports `completed`.
|
|
739
|
+
//
|
|
740
|
+
// `streamReportedDone` keeps that from swallowing a run that genuinely finished: an adapter
|
|
741
|
+
// that ignores the signal and drains after reporting `hasMore: false` delivered everything it
|
|
742
|
+
// had, even when the cancel landed during the final read. Calling that cancelled would tell
|
|
743
|
+
// the operator a complete sync was partial and leave a finished run resumable.
|
|
744
|
+
if (cancellation.signal.aborted && !streamReportedDone) {
|
|
745
|
+
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
|
|
674
749
|
await finalizeRun(run.id, 'completed', scope, undefined, operationalTelemetry)
|
|
675
750
|
},
|
|
676
751
|
|
|
@@ -753,10 +828,17 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
753
828
|
const mapping = await resolveMapping(adapter, run.entityType, scope)
|
|
754
829
|
let processedCount = await seedProcessedCount(run.progressJobId, scope)
|
|
755
830
|
let committedBatches = activeRun.batchesCompleted ?? 0
|
|
831
|
+
// Whether the last committed batch said the source was exhausted. Distinguishes a stream that
|
|
832
|
+
// drained from one the adapter stopped early — see the post-stream finalize below.
|
|
833
|
+
let streamReportedDone = false
|
|
756
834
|
// Captured while the triggering job's span is still the active one, so
|
|
757
835
|
// every rooted batch trace can link back to it.
|
|
758
836
|
const runTrace = captureTelemetryTrace()
|
|
759
837
|
const spanAttributes = runSpanAttributes(run, providerKey, scope)
|
|
838
|
+
// Declared outside the try because both the catch and the completion path below read it.
|
|
839
|
+
const cancellation = new AbortController()
|
|
840
|
+
const heartbeat = makeHeartbeatTick(run.progressJobId, scope)
|
|
841
|
+
const pollCancellation = makeCancellationTick(run.progressJobId, scope, cancellation)
|
|
760
842
|
|
|
761
843
|
try {
|
|
762
844
|
const streamResult = await forEachBatch(
|
|
@@ -770,8 +852,11 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
770
852
|
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
771
853
|
runId: run.id,
|
|
772
854
|
parameters: (run.parameters ?? {}) as RunParameters,
|
|
855
|
+
signal: cancellation.signal,
|
|
773
856
|
}),
|
|
774
|
-
|
|
857
|
+
// `finally` so a synchronous throw from the heartbeat cannot also stop cancellation
|
|
858
|
+
// from being observed for the rest of the run.
|
|
859
|
+
() => { try { heartbeat() } finally { pollCancellation() } },
|
|
775
860
|
HEARTBEAT_TICK_MS,
|
|
776
861
|
),
|
|
777
862
|
{
|
|
@@ -786,7 +871,7 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
786
871
|
'data_sync.batch_size': batch.results.length,
|
|
787
872
|
})
|
|
788
873
|
|
|
789
|
-
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
874
|
+
if (cancellation.signal.aborted || (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId))) {
|
|
790
875
|
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
791
876
|
return 'stop'
|
|
792
877
|
}
|
|
@@ -815,6 +900,7 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
815
900
|
{ expectedBatchesCompleted: committedBatches, persistSharedCursor },
|
|
816
901
|
)
|
|
817
902
|
committedBatches += 1
|
|
903
|
+
streamReportedDone = batch.hasMore === false
|
|
818
904
|
await updateProgress(run.progressJobId, processedCount, null, scope)
|
|
819
905
|
await logExportItemFailures(run.id, run.integrationId, batch.results, scope)
|
|
820
906
|
|
|
@@ -846,6 +932,15 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
846
932
|
})
|
|
847
933
|
return
|
|
848
934
|
}
|
|
935
|
+
// An adapter that honours the signal may reject instead of returning, so our own abort is
|
|
936
|
+
// a cancellation rather than a fault — and must not leave an `error` entry in the
|
|
937
|
+
// integration log for a run the operator cancelled on purpose. Anything else that merely
|
|
938
|
+
// coincided with the cancel — a rejecting commit, an upstream 500 — is a genuine failure
|
|
939
|
+
// and keeps its log entry, its message, its `failed` status and its failed event.
|
|
940
|
+
if (cancellation.signal.aborted && isAbortError(error)) {
|
|
941
|
+
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
942
|
+
return
|
|
943
|
+
}
|
|
849
944
|
const message = error instanceof Error ? error.message : 'Sync export failed'
|
|
850
945
|
await integrationLogService.write(
|
|
851
946
|
{
|
|
@@ -860,6 +955,19 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
860
955
|
return
|
|
861
956
|
}
|
|
862
957
|
|
|
958
|
+
// An adapter that honours the signal stops mid-batch and returns WITHOUT yielding, so the
|
|
959
|
+
// batch handler — which owns the only other `cancelled` transition — never runs and the
|
|
960
|
+
// stream reports `completed`.
|
|
961
|
+
//
|
|
962
|
+
// `streamReportedDone` keeps that from swallowing a run that genuinely finished: an adapter
|
|
963
|
+
// that ignores the signal and drains after reporting `hasMore: false` delivered everything it
|
|
964
|
+
// had, even when the cancel landed during the final read. Calling that cancelled would tell
|
|
965
|
+
// the operator a complete sync was partial and leave a finished run resumable.
|
|
966
|
+
if (cancellation.signal.aborted && !streamReportedDone) {
|
|
967
|
+
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
|
|
863
971
|
await finalizeRun(run.id, 'completed', scope, undefined, operationalTelemetry)
|
|
864
972
|
},
|
|
865
973
|
}
|
|
@@ -1058,6 +1058,11 @@ export const syncExcelCustomersAdapter: DataSyncAdapter = {
|
|
|
1058
1058
|
const items: ImportItem[] = []
|
|
1059
1059
|
|
|
1060
1060
|
for (let index = 0; index < batchRows.length; index += 1) {
|
|
1061
|
+
// Above the yield, so an abandoned page is never yielded and its cursor never committed —
|
|
1062
|
+
// the rows applied so far are re-applied on resume, which the replay-safety contract on
|
|
1063
|
+
// `streamImport` already requires. Each row goes through the command bus, so a large page
|
|
1064
|
+
// is exactly the case where the operator would otherwise wait out the whole batch.
|
|
1065
|
+
if (input.signal?.aborted) return
|
|
1061
1066
|
items.push(await processRow({
|
|
1062
1067
|
row: batchRows[index],
|
|
1063
1068
|
rowNumber: batch.rowStart + index + 1,
|