@open-mercato/core 0.6.8-develop.6992.1.00c90fecff → 0.6.8-develop.6998.1.d2bc46c56c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/sync_run/index.js +2 -0
  3. package/dist/generated/entities/sync_run/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/auth/api/sidebar/preferences/route.js +2 -2
  7. package/dist/modules/auth/api/sidebar/preferences/route.js.map +1 -1
  8. package/dist/modules/auth/lib/backendChrome.js +3 -3
  9. package/dist/modules/auth/lib/backendChrome.js.map +1 -1
  10. package/dist/modules/auth/services/sidebarPreferencesService.js +7 -2
  11. package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
  12. package/dist/modules/data_sync/api/options.js +1 -0
  13. package/dist/modules/data_sync/api/options.js.map +2 -2
  14. package/dist/modules/data_sync/api/run.js +15 -1
  15. package/dist/modules/data_sync/api/run.js.map +2 -2
  16. package/dist/modules/data_sync/api/runs/[id]/retry.js +23 -1
  17. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  18. package/dist/modules/data_sync/api/runs/[id]/route.js +1 -0
  19. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  20. package/dist/modules/data_sync/backend/data-sync/page.js +60 -17
  21. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  22. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +34 -2
  23. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  24. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +21 -15
  25. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  26. package/dist/modules/data_sync/components/RunParameterFields.js +135 -0
  27. package/dist/modules/data_sync/components/RunParameterFields.js.map +7 -0
  28. package/dist/modules/data_sync/data/entities.js +3 -0
  29. package/dist/modules/data_sync/data/entities.js.map +2 -2
  30. package/dist/modules/data_sync/data/validators.js +4 -1
  31. package/dist/modules/data_sync/data/validators.js.map +2 -2
  32. package/dist/modules/data_sync/lib/run-parameters.js +108 -0
  33. package/dist/modules/data_sync/lib/run-parameters.js.map +7 -0
  34. package/dist/modules/data_sync/lib/start-run.js +1 -0
  35. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  36. package/dist/modules/data_sync/lib/sync-engine.js +4 -2
  37. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-run-service.js +1 -0
  39. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  40. package/dist/modules/data_sync/migrations/Migration20260810120000.js +13 -0
  41. package/dist/modules/data_sync/migrations/Migration20260810120000.js.map +7 -0
  42. package/dist/modules/data_sync/workers/sync-scheduled.js +23 -2
  43. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  44. package/dist/modules/query_index/di.js +125 -48
  45. package/dist/modules/query_index/di.js.map +2 -2
  46. package/dist/modules/sales/api/documents/factory.js +39 -1
  47. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  48. package/dist/modules/sales/commands/documents.js +26 -5
  49. package/dist/modules/sales/commands/documents.js.map +2 -2
  50. package/generated/entities/sync_run/index.ts +1 -0
  51. package/generated/entity-fields-registry.ts +1 -0
  52. package/package.json +7 -7
  53. package/src/modules/auth/api/sidebar/preferences/route.ts +2 -2
  54. package/src/modules/auth/lib/backendChrome.tsx +2 -2
  55. package/src/modules/auth/services/sidebarPreferencesService.ts +22 -3
  56. package/src/modules/data_sync/AGENTS.md +83 -8
  57. package/src/modules/data_sync/api/options.ts +1 -0
  58. package/src/modules/data_sync/api/run.ts +17 -0
  59. package/src/modules/data_sync/api/runs/[id]/retry.ts +32 -1
  60. package/src/modules/data_sync/api/runs/[id]/route.ts +1 -0
  61. package/src/modules/data_sync/backend/data-sync/page.tsx +73 -17
  62. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +62 -2
  63. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +28 -15
  64. package/src/modules/data_sync/components/RunParameterFields.tsx +229 -0
  65. package/src/modules/data_sync/data/entities.ts +4 -1
  66. package/src/modules/data_sync/data/validators.ts +3 -0
  67. package/src/modules/data_sync/i18n/de.json +10 -0
  68. package/src/modules/data_sync/i18n/en.json +10 -0
  69. package/src/modules/data_sync/i18n/es.json +10 -0
  70. package/src/modules/data_sync/i18n/ko.json +10 -0
  71. package/src/modules/data_sync/i18n/pl.json +10 -0
  72. package/src/modules/data_sync/lib/adapter.ts +88 -0
  73. package/src/modules/data_sync/lib/run-parameters.ts +196 -0
  74. package/src/modules/data_sync/lib/start-run.ts +2 -0
  75. package/src/modules/data_sync/lib/sync-engine.ts +5 -1
  76. package/src/modules/data_sync/lib/sync-run-service.ts +2 -0
  77. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +17 -1
  78. package/src/modules/data_sync/migrations/Migration20260810120000.ts +13 -0
  79. package/src/modules/data_sync/workers/sync-scheduled.ts +32 -1
  80. package/src/modules/query_index/di.ts +153 -56
  81. package/src/modules/sales/api/documents/factory.ts +62 -1
  82. package/src/modules/sales/commands/documents.ts +40 -4
  83. package/src/modules/sales/i18n/de.json +1 -0
  84. package/src/modules/sales/i18n/en.json +1 -0
  85. package/src/modules/sales/i18n/es.json +1 -0
  86. package/src/modules/sales/i18n/ko.json +1 -0
  87. package/src/modules/sales/i18n/pl.json +1 -0
@@ -0,0 +1,108 @@
1
+ const RESERVED_PARAMETER_KEYS = /* @__PURE__ */ new Set(["__proto__"]);
2
+ function isReservedRunParameterKey(key) {
3
+ return RESERVED_PARAMETER_KEYS.has(key);
4
+ }
5
+ function getApplicableRunParameters(declared, direction, entityType) {
6
+ if (!declared || declared.length === 0) return [];
7
+ return declared.filter((param) => {
8
+ if (isReservedRunParameterKey(param.key)) return false;
9
+ if (param.direction && param.direction !== direction) return false;
10
+ if (param.entityType !== void 0 && entityType !== void 0) {
11
+ const allowed = Array.isArray(param.entityType) ? param.entityType : [param.entityType];
12
+ if (!allowed.includes(entityType)) return false;
13
+ }
14
+ return true;
15
+ });
16
+ }
17
+ function isBlank(value) {
18
+ return value === void 0 || value === null || typeof value === "string" && value.trim().length === 0;
19
+ }
20
+ function coerceBoolean(value) {
21
+ if (typeof value === "boolean") return value;
22
+ if (typeof value === "number") {
23
+ if (value === 1) return true;
24
+ if (value === 0) return false;
25
+ return null;
26
+ }
27
+ if (typeof value === "string") {
28
+ const normalized = value.trim().toLowerCase();
29
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
30
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
31
+ }
32
+ return null;
33
+ }
34
+ function coerceNumber(value) {
35
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
36
+ if (typeof value === "string") {
37
+ const trimmed = value.trim();
38
+ if (trimmed.length === 0) return null;
39
+ const parsed = Number(trimmed);
40
+ return Number.isFinite(parsed) ? parsed : null;
41
+ }
42
+ return null;
43
+ }
44
+ function buildRunParameterError(param, code, extra = {}) {
45
+ const message = code === "required" ? `${param.label} is required.` : code === "type" ? `${param.label} must be a ${extra.type}.` : code === "min" ? `${param.label} must be at least ${extra.min}.` : code === "max" ? `${param.label} must be at most ${extra.max}.` : `${param.label} must be one of: ${extra.options}.`;
46
+ return { key: param.key, code, params: { label: param.label, ...extra }, message };
47
+ }
48
+ function coerceDeclaredValue(param, raw) {
49
+ switch (param.type) {
50
+ case "boolean": {
51
+ const coerced = coerceBoolean(raw);
52
+ if (coerced === null) return { ok: false, error: buildRunParameterError(param, "type", { type: "boolean" }) };
53
+ return { ok: true, value: coerced };
54
+ }
55
+ case "number": {
56
+ const coerced = coerceNumber(raw);
57
+ if (coerced === null) return { ok: false, error: buildRunParameterError(param, "type", { type: "number" }) };
58
+ if (typeof param.min === "number" && coerced < param.min) {
59
+ return { ok: false, error: buildRunParameterError(param, "min", { min: param.min }) };
60
+ }
61
+ if (typeof param.max === "number" && coerced > param.max) {
62
+ return { ok: false, error: buildRunParameterError(param, "max", { max: param.max }) };
63
+ }
64
+ return { ok: true, value: coerced };
65
+ }
66
+ case "select": {
67
+ const candidate = String(raw);
68
+ const allowed = (param.options ?? []).map((option) => option.value);
69
+ if (!allowed.includes(candidate)) {
70
+ return { ok: false, error: buildRunParameterError(param, "select", { options: allowed.join(", ") }) };
71
+ }
72
+ return { ok: true, value: candidate };
73
+ }
74
+ case "string":
75
+ default:
76
+ return { ok: true, value: String(raw).trim() };
77
+ }
78
+ }
79
+ function normalizeRunParameters(declared, direction, raw, entityType) {
80
+ const params = getApplicableRunParameters(declared, direction, entityType);
81
+ const input = raw && typeof raw === "object" ? raw : {};
82
+ const values = {};
83
+ const errors = [];
84
+ for (const param of params) {
85
+ const provided = Object.prototype.hasOwnProperty.call(input, param.key) ? input[param.key] : void 0;
86
+ if (isBlank(provided)) {
87
+ if (param.defaultValue === void 0) {
88
+ if (param.required) errors.push(buildRunParameterError(param, "required"));
89
+ continue;
90
+ }
91
+ const fallback = coerceDeclaredValue(param, param.defaultValue);
92
+ if (fallback.ok) values[param.key] = fallback.value;
93
+ else errors.push(fallback.error);
94
+ continue;
95
+ }
96
+ const coerced = coerceDeclaredValue(param, provided);
97
+ if (coerced.ok) values[param.key] = coerced.value;
98
+ else errors.push(coerced.error);
99
+ }
100
+ if (errors.length > 0) return { ok: false, errors };
101
+ return { ok: true, values };
102
+ }
103
+ export {
104
+ getApplicableRunParameters,
105
+ isReservedRunParameterKey,
106
+ normalizeRunParameters
107
+ };
108
+ //# sourceMappingURL=run-parameters.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/data_sync/lib/run-parameters.ts"],
4
+ "sourcesContent": ["import type { RunParameter, RunParameterValue } from './adapter'\n\nexport type RunParameterErrorCode = 'required' | 'type' | 'min' | 'max' | 'select'\n\nexport type RunParameterError = {\n key: string\n /**\n * Machine-readable reason, so the client can render a translated message.\n * The dashboard maps these to `data_sync.runParameters.errors.<code>`.\n */\n code: RunParameterErrorCode\n /** Values the translated message interpolates (label, bound, option list). */\n params: { label: string; type?: string; min?: number; max?: number; options?: string }\n /**\n * Pre-rendered English sentence. Kept for non-UI callers (API clients, logs)\n * and as the client-side fallback; the UI prefers `code` + `params`.\n */\n message: string\n}\n\nexport type NormalizeRunParametersResult =\n | { ok: true; values: Record<string, RunParameterValue> }\n | { ok: false; errors: RunParameterError[] }\n\n/**\n * Keys that cannot round-trip through a plain object literal. Assigning a\n * primitive to `__proto__` is silently discarded rather than creating an own\n * property, so a parameter declared under that key would vanish between the\n * form and the adapter instead of failing loudly. Rejecting it here \u2014 the one\n * place every surface funnels through \u2014 keeps the dashboard, the default-value\n * builder and the normalizer agreeing on the same parameter set.\n *\n * (`constructor` / `prototype` assign normally and are left alone.)\n */\nconst RESERVED_PARAMETER_KEYS = new Set(['__proto__'])\n\nexport function isReservedRunParameterKey(key: string): boolean {\n return RESERVED_PARAMETER_KEYS.has(key)\n}\n\n/**\n * Returns the declared parameters that apply to a given run. A parameter\n * without an explicit `direction` applies to both directions; one without an\n * explicit `entityType` applies to every entity. When `entityType` is omitted\n * here (the caller does not know the run's entity), entity scoping is skipped.\n * Parameters declared under a reserved key are dropped.\n */\nexport function getApplicableRunParameters(\n declared: RunParameter[] | undefined,\n direction: 'import' | 'export',\n entityType?: string,\n): RunParameter[] {\n if (!declared || declared.length === 0) return []\n return declared.filter((param) => {\n if (isReservedRunParameterKey(param.key)) return false\n if (param.direction && param.direction !== direction) return false\n if (param.entityType !== undefined && entityType !== undefined) {\n const allowed = Array.isArray(param.entityType) ? param.entityType : [param.entityType]\n if (!allowed.includes(entityType)) return false\n }\n return true\n })\n}\n\nfunction isBlank(value: unknown): boolean {\n return value === undefined || value === null || (typeof value === 'string' && value.trim().length === 0)\n}\n\nfunction coerceBoolean(value: unknown): boolean | null {\n if (typeof value === 'boolean') return value\n if (typeof value === 'number') {\n if (value === 1) return true\n if (value === 0) return false\n return null\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase()\n if (['true', '1', 'yes', 'on'].includes(normalized)) return true\n if (['false', '0', 'no', 'off'].includes(normalized)) return false\n }\n return null\n}\n\nfunction coerceNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string') {\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 return null\n}\n\n/**\n * Validate and coerce an untrusted parameter object against an adapter's\n * declared `runParameters` for the given run direction and entity type.\n *\n * - Parameters that do not apply to the direction or entity type are ignored.\n * - Undeclared keys in the input are dropped (never passed through).\n * - Blank values fall back to `defaultValue`; a blank required value is an error.\n * - Values are coerced to the declared type; the result only contains declared keys.\n */\nfunction buildRunParameterError(\n param: RunParameter,\n code: RunParameterErrorCode,\n extra: { type?: string; min?: number; max?: number; options?: string } = {},\n): RunParameterError {\n const message = code === 'required' ? `${param.label} is required.`\n : code === 'type' ? `${param.label} must be a ${extra.type}.`\n : code === 'min' ? `${param.label} must be at least ${extra.min}.`\n : code === 'max' ? `${param.label} must be at most ${extra.max}.`\n : `${param.label} must be one of: ${extra.options}.`\n return { key: param.key, code, params: { label: param.label, ...extra }, message }\n}\n\ntype CoerceResult =\n | { ok: true; value: RunParameterValue }\n | { ok: false; error: RunParameterError }\n\n/** Coerces and range-checks one non-blank value against its declaration. */\nfunction coerceDeclaredValue(param: RunParameter, raw: unknown): CoerceResult {\n switch (param.type) {\n case 'boolean': {\n const coerced = coerceBoolean(raw)\n if (coerced === null) return { ok: false, error: buildRunParameterError(param, 'type', { type: 'boolean' }) }\n return { ok: true, value: coerced }\n }\n case 'number': {\n const coerced = coerceNumber(raw)\n if (coerced === null) return { ok: false, error: buildRunParameterError(param, 'type', { type: 'number' }) }\n if (typeof param.min === 'number' && coerced < param.min) {\n return { ok: false, error: buildRunParameterError(param, 'min', { min: param.min }) }\n }\n if (typeof param.max === 'number' && coerced > param.max) {\n return { ok: false, error: buildRunParameterError(param, 'max', { max: param.max }) }\n }\n return { ok: true, value: coerced }\n }\n case 'select': {\n const candidate = String(raw)\n const allowed = (param.options ?? []).map((option) => option.value)\n if (!allowed.includes(candidate)) {\n return { ok: false, error: buildRunParameterError(param, 'select', { options: allowed.join(', ') }) }\n }\n return { ok: true, value: candidate }\n }\n case 'string':\n default:\n return { ok: true, value: String(raw).trim() }\n }\n}\n\nexport function normalizeRunParameters(\n declared: RunParameter[] | undefined,\n direction: 'import' | 'export',\n raw: Record<string, unknown> | null | undefined,\n entityType?: string,\n): NormalizeRunParametersResult {\n const params = getApplicableRunParameters(declared, direction, entityType)\n const input = raw && typeof raw === 'object' ? raw : {}\n const values: Record<string, RunParameterValue> = {}\n const errors: RunParameterError[] = []\n\n for (const param of params) {\n // Own-property lookup only: a parameter declared under an inherited name\n // (`constructor`, `toString`, `valueOf`) would otherwise read back the\n // inherited function instead of `undefined`, so a required check would\n // silently pass and a string parameter would coerce the function source.\n const provided = Object.prototype.hasOwnProperty.call(input, param.key)\n ? (input as Record<string, unknown>)[param.key]\n : undefined\n\n if (isBlank(provided)) {\n if (param.defaultValue === undefined) {\n if (param.required) errors.push(buildRunParameterError(param, 'required'))\n continue\n }\n // The declared default goes through the same checks as a submitted\n // value, so a misdeclared adapter (a `select` default outside its own\n // options, a number below its own `min`) fails loudly here rather than\n // shipping an invalid value to itself mid-run.\n const fallback = coerceDeclaredValue(param, param.defaultValue)\n if (fallback.ok) values[param.key] = fallback.value\n else errors.push(fallback.error)\n continue\n }\n\n const coerced = coerceDeclaredValue(param, provided)\n if (coerced.ok) values[param.key] = coerced.value\n else errors.push(coerced.error)\n }\n\n if (errors.length > 0) return { ok: false, errors }\n return { ok: true, values }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -29,6 +29,7 @@ async function startDataSyncRun(params) {
29
29
  direction: input.direction,
30
30
  cursor: input.cursor ?? null,
31
31
  triggeredBy: input.triggeredBy ?? scope.userId ?? null,
32
+ parameters: input.parameters ?? null,
32
33
  progressJobId: progressJob?.id ?? null
33
34
  },
34
35
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/start-run.ts"],
4
- "sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\nimport { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from './queue-policy'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
5
- "mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,8BAA8B;AAyB/D,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,yBAAyB;AAC1E,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\nimport { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from './queue-policy'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n parameters?: Record<string, unknown> | null\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n parameters: input.parameters ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
5
+ "mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,8BAA8B;AA0B/D,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,YAAY,MAAM,cAAc;AAAA,MAChC,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,yBAAyB;AAC1E,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -418,7 +418,8 @@ function createSyncEngine(deps) {
418
418
  credentials,
419
419
  mapping,
420
420
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
421
- runId: run.id
421
+ runId: run.id,
422
+ parameters: run.parameters ?? {}
422
423
  }),
423
424
  makeHeartbeatTick(run.progressJobId, scope),
424
425
  HEARTBEAT_TICK_MS
@@ -565,7 +566,8 @@ function createSyncEngine(deps) {
565
566
  credentials,
566
567
  mapping,
567
568
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
568
- runId: run.id
569
+ runId: run.id,
570
+ parameters: run.parameters ?? {}
569
571
  }),
570
572
  makeHeartbeatTick(run.progressJobId, scope),
571
573
  HEARTBEAT_TICK_MS
@@ -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 { getIntegration } from '@open-mercato/shared/modules/integrations/types'\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 } from './adapter'\nimport { getDataSyncAdapter, resolveProviderKey } from './adapter-registry'\nimport type { SyncRunService } from './sync-run-service'\nimport { SyncRunOwnershipConflictError } from './sync-run-service'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-engine' })\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\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\n try {\n for await (const batch of 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 }),\n makeHeartbeatTick(run.progressJobId, scope),\n HEARTBEAT_TICK_MS,\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\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 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 } 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\n try {\n for await (const batch of 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 }),\n makeHeartbeatTick(run.progressJobId, scope),\n HEARTBEAT_TICK_MS,\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\n }\n\n const delta = applyExportCounters(batch)\n processedCount += delta.processedCount\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 } 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 { getIntegration } from '@open-mercato/shared/modules/integrations/types'\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 { createLogger } from '@open-mercato/shared/lib/logger'\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\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\n try {\n for await (const batch of 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 if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return\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 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 } 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\n try {\n for await (const batch of 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 if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {\n await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)\n return\n }\n\n const delta = applyExportCounters(batch)\n processedCount += delta.processedCount\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 } 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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",
6
6
  "names": []
7
7
  }
@@ -69,6 +69,7 @@ function createSyncRunService(em) {
69
69
  triggeredBy: input.triggeredBy,
70
70
  progressJobId: input.progressJobId,
71
71
  jobId: input.jobId,
72
+ parameters: input.parameters ?? null,
72
73
  organizationId: scope.organizationId,
73
74
  tenantId: scope.tenantId
74
75
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/sync-run-service.ts"],
4
- "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { findAndCountWithDecryption, findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { SyncCursor, SyncRun } from '../data/entities'\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nfunction buildRunSearchFilter(search: string): FilterQuery<SyncRun>[] | null {\n const trimmed = search.trim()\n if (!trimmed) return null\n const pattern = `%${escapeLikePattern(trimmed)}%`\n const conditions: FilterQuery<SyncRun>[] = [\n { integrationId: { $ilike: pattern } },\n { entityType: { $ilike: pattern } },\n { status: { $ilike: pattern } },\n ]\n if (UUID_PATTERN.test(trimmed)) {\n conditions.push({ id: trimmed })\n }\n return conditions\n}\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type CursorCommitOptions = {\n /**\n * Mirror the committed cursor into the shared `sync_cursors` row. Defaults to\n * `true`; the engine passes the adapter's `persistsSharedCursor(entityType)`\n * verdict. `false` keeps the cursor on the run row alone.\n */\n persistSharedCursor?: boolean\n /**\n * Fences the write against a concurrent delivery: the run must still be\n * `running` and still sit on this batch count, or the commit throws\n * {@link SyncRunOwnershipConflictError} and rolls back. Omit to keep the\n * unguarded write for callers outside the engine.\n */\n expectedBatchesCompleted?: number\n}\n\n/** {@link CursorCommitOptions} minus the fence, which `updateCursor` does not apply. */\nexport type SharedCursorOption = Pick<CursorCommitOptions, 'persistSharedCursor'>\n\n/**\n * Raised when a batch commit loses the ownership compare-and-swap, meaning\n * another delivery of the same job advanced the run while this worker was\n * streaming.\n *\n * BullMQ guarantees at-least-once delivery: a job whose lock is not renewed is\n * redelivered under the SAME job id, whether the previous worker died or is only\n * blocked. No identity token can tell those apart, so ownership is enforced here\n * \u2014 on the write that matters \u2014 instead of at claim time. The loser aborts and\n * leaves the run to the worker that is still making progress.\n */\nexport class SyncRunOwnershipConflictError extends Error {\n constructor(\n readonly runId: string,\n readonly expectedBatchesCompleted: number,\n ) {\n super(`[internal] Sync run ${runId} advanced past batch ${expectedBatchesCompleted} under a concurrent worker`)\n this.name = 'SyncRunOwnershipConflictError'\n }\n}\n\nexport function createSyncRunService(em: EntityManager) {\n async function resolveCursorRow(run: SyncRun, scope: SyncScope): Promise<SyncCursor | null> {\n return findOneWithDecryption(\n em,\n SyncCursor,\n {\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n }\n\n function applyCursorMutation(\n run: SyncRun,\n cursorRow: SyncCursor | null,\n cursor: string,\n scope: SyncScope,\n persistSharedCursor: boolean,\n ): void {\n run.cursor = cursor\n if (!persistSharedCursor) return\n if (cursorRow) {\n cursorRow.cursor = cursor\n } else {\n em.create(SyncCursor, {\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n cursor,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n }\n }\n\n return {\n async createRun(input: {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n progressJobId?: string | null\n jobId?: string | null\n }, scope: SyncScope): Promise<SyncRun> {\n const row = em.create(SyncRun, {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n status: 'pending',\n cursor: input.cursor,\n initialCursor: input.cursor,\n triggeredBy: input.triggeredBy,\n progressJobId: input.progressJobId,\n jobId: input.jobId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n await em.persist(row).flush()\n return row\n },\n\n async getRun(runId: string, scope: SyncScope): Promise<SyncRun | null> {\n return findOneWithDecryption(\n em,\n SyncRun,\n {\n id: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async listRuns(query: {\n integrationId?: string\n entityType?: string\n direction?: 'import' | 'export'\n status?: string\n search?: string\n page: number\n pageSize: number\n }, scope: SyncScope): Promise<{ items: SyncRun[]; total: number }> {\n const where: FilterQuery<SyncRun> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (query.integrationId) where.integrationId = query.integrationId\n if (query.entityType) where.entityType = query.entityType\n if (query.direction) where.direction = query.direction\n if (query.status) where.status = query.status as SyncRun['status']\n if (query.search) {\n const searchConditions = buildRunSearchFilter(query.search)\n if (searchConditions) where.$or = searchConditions\n }\n\n const [items, total] = await findAndCountWithDecryption(\n em,\n SyncRun,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n scope,\n )\n\n return { items, total }\n },\n\n async markStatus(runId: string, status: SyncRun['status'], scope: SyncScope, error?: string): Promise<SyncRun | null> {\n if (status === 'running') {\n const updated = await em.nativeUpdate(\n SyncRun,\n {\n id: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n // A BullMQ stalled-job redelivery finds the run in `running` after\n // the previous worker was hard-killed. Treat that transition as an\n // idempotent claim while still excluding terminal states so a\n // cancelled or completed run cannot be revived.\n status: { $in: ['pending', 'running'] },\n },\n {\n status,\n ...(error !== undefined ? { lastError: error } : {}),\n updatedAt: new Date(),\n },\n )\n if (updated === 0) return null\n const row = await this.getRun(runId, scope)\n if (row && typeof em.refresh === 'function') {\n await em.refresh(row)\n }\n return row\n }\n\n const row = await this.getRun(runId, scope)\n if (!row) return null\n const isTerminal = row.status === 'completed' || row.status === 'failed' || row.status === 'cancelled'\n if (isTerminal && row.status !== status) {\n return row\n }\n row.status = status\n if (error !== undefined) row.lastError = error\n await em.flush()\n return row\n },\n\n /**\n * @deprecated Use {@link commitBatchProgress}, which writes counters and\n * cursor in one transaction behind the ownership fence. This method updates\n * counters unfenced, so two deliveries of the same job can lose each other's\n * increments. Kept for external callers only.\n */\n async updateCounts(\n runId: string,\n delta: Partial<Pick<SyncRun, 'createdCount' | 'updatedCount' | 'skippedCount' | 'failedCount' | 'batchesCompleted'>>,\n scope: SyncScope,\n ): Promise<SyncRun | null> {\n const row = await this.getRun(runId, scope)\n if (!row) return null\n\n row.createdCount += delta.createdCount ?? 0\n row.updatedCount += delta.updatedCount ?? 0\n row.skippedCount += delta.skippedCount ?? 0\n row.failedCount += delta.failedCount ?? 0\n row.batchesCompleted += delta.batchesCompleted ?? 0\n await em.flush()\n return row\n },\n\n /**\n * @deprecated Use {@link commitBatchProgress}. This method advances the\n * cursor without the ownership fence, so a stale delivery can move the\n * cursor of a run another worker owns. Kept for external callers only.\n *\n * It still takes `persistSharedCursor` despite being deprecated: an external\n * caller advancing the cursor of an opted-out entity type would otherwise\n * create the very `sync_cursors` row the opt-out exists to avoid, and a\n * later incremental run would read it as a start position. The deprecated\n * path has to honour the opt-out for as long as it exists.\n */\n async updateCursor(runId: string, cursor: string, scope: SyncScope, options?: SharedCursorOption): Promise<void> {\n const run = await this.getRun(runId, scope)\n if (!run) return\n const persistSharedCursor = options?.persistSharedCursor ?? true\n const cursorRow = persistSharedCursor ? await resolveCursorRow(run, scope) : null\n await withAtomicFlush(em, [\n () => applyCursorMutation(run, cursorRow, cursor, scope, persistSharedCursor),\n ], { transaction: true })\n },\n\n /**\n * Commits one batch's counters and cursor in a single transaction.\n *\n * Passing `options.expectedBatchesCompleted` fences the write: the run must\n * still be `running` and still sit on that batch count, or another delivery\n * of the same BullMQ job owns the run and this commit throws\n * `SyncRunOwnershipConflictError` and rolls back. Omitting it keeps the\n * legacy unguarded write for callers outside the engine.\n *\n * `options.persistSharedCursor` is orthogonal to the fence: it decides\n * whether the committed cursor is mirrored into the shared `sync_cursors`\n * row, and the two compose freely \u2014 a fenced commit for an opted-out entity\n * type advances the run row alone and still throws on a stale fence.\n *\n * The fence token is `batchesCompleted` rather than `cursor` because it\n * advances by construction on every commit. A cursor is a free-form adapter\n * string that an adapter may legitimately repeat between batches \u2014 the\n * Akeneo products adapter does, between its final page and the\n * reconciliation batch that follows it \u2014 and a repeated token fences\n * nothing.\n *\n * The guard's `UPDATE` also holds the row lock for the rest of the\n * transaction, so a competing commit blocks here and then re-reads the\n * advanced count instead of interleaving with this one. That is what lets\n * the counters below stay a plain read-modify-write against the snapshot\n * read above: a commit that wins the fence has proven that nothing else\n * landed since it read.\n */\n async commitBatchProgress(\n runId: string,\n delta: Partial<Pick<SyncRun, 'createdCount' | 'updatedCount' | 'skippedCount' | 'failedCount' | 'batchesCompleted'>>,\n cursor: string,\n scope: SyncScope,\n options?: CursorCommitOptions,\n ): Promise<SyncRun | null> {\n const run = await this.getRun(runId, scope)\n if (!run) return null\n const { expectedBatchesCompleted } = options ?? {}\n const persistSharedCursor = options?.persistSharedCursor ?? true\n const cursorRow = persistSharedCursor ? await resolveCursorRow(run, scope) : null\n const claimRunOwnership = async () => {\n if ((delta.batchesCompleted ?? 0) < 1) {\n throw new Error(`[internal] A fenced commit for sync run ${runId} must advance batchesCompleted`)\n }\n const owned = await em.nativeUpdate(\n SyncRun,\n {\n id: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n status: 'running',\n batchesCompleted: expectedBatchesCompleted,\n },\n { updatedAt: new Date() },\n )\n if (owned === 0) {\n throw new SyncRunOwnershipConflictError(runId, expectedBatchesCompleted ?? 0)\n }\n }\n await withAtomicFlush(em, [\n ...(expectedBatchesCompleted === undefined ? [] : [claimRunOwnership]),\n () => {\n run.createdCount += delta.createdCount ?? 0\n run.updatedCount += delta.updatedCount ?? 0\n run.skippedCount += delta.skippedCount ?? 0\n run.failedCount += delta.failedCount ?? 0\n run.batchesCompleted += delta.batchesCompleted ?? 0\n applyCursorMutation(run, cursorRow, cursor, scope, persistSharedCursor)\n },\n ], { transaction: true })\n return run\n },\n\n async resolveCursor(integrationId: string, entityType: string, direction: 'import' | 'export', scope: SyncScope): Promise<string | null> {\n const row = await findOneWithDecryption(\n em,\n SyncCursor,\n {\n integrationId,\n entityType,\n direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n return row?.cursor ?? null\n },\n\n /**\n * Resume position for an entity type whose adapter opted out of the shared\n * `sync_cursors` row: the cursor of the most recent run, unless that run\n * reached `completed`. A finished walk resumes from `null` so the next run\n * starts over rather than skipping everything an older interrupted run had\n * already passed.\n */\n async resolveResumeCursor(integrationId: string, entityType: string, direction: 'import' | 'export', scope: SyncScope): Promise<string | null> {\n const [run] = await findWithDecryption(\n em,\n SyncRun,\n {\n integrationId,\n entityType,\n direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'DESC' }, limit: 1 },\n scope,\n )\n if (!run || run.status === 'completed') return null\n return run.cursor ?? null\n },\n\n /**\n * Clears the run-scoped resume position for an entity type, so the next\n * non-`fullSync` run starts from the beginning. Returns how many runs were\n * cleared.\n *\n * This is the opt-out's equivalent of deleting the shared `sync_cursors`\n * row. An entity type whose adapter returns `false` from\n * `persistsSharedCursor` has no such row, so a reset flow that only deletes\n * `SyncCursor` would leave {@link resolveResumeCursor} returning the cursor\n * of the interrupted run it just reset against \u2014 re-importing the tail of a\n * walk instead of the whole thing. Reset flows MUST call this alongside\n * their `SyncCursor` delete; it is a no-op when nothing is interrupted.\n *\n * The `status` filter here selects which rows to clear. It is deliberately\n * NOT the read-side filter {@link resolveResumeCursor} avoids: that method\n * reads the single most recent run whatever its status, precisely so an\n * older interrupted run cannot outlive a later completed walk. Clearing\n * every interrupted run is enough to start fresh either way \u2014 if the latest\n * run was interrupted its cursor is now null, and if it completed the resume\n * path already returns null.\n */\n async resetResumePosition(\n integrationId: string,\n entityType: string,\n direction: 'import' | 'export',\n scope: SyncScope,\n ): Promise<number> {\n return em.nativeUpdate(\n SyncRun,\n {\n integrationId,\n entityType,\n direction,\n status: { $ne: 'completed' },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { cursor: null, updatedAt: new Date() },\n )\n },\n\n async findRunningOverlap(integrationId: string, entityType: string, direction: 'import' | 'export', scope: SyncScope): Promise<SyncRun | null> {\n const [run] = await findWithDecryption(\n em,\n SyncRun,\n {\n integrationId,\n entityType,\n direction,\n status: { $in: ['pending', 'running'] },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { limit: 1 },\n scope,\n )\n return run ?? null\n },\n }\n}\n\nexport type SyncRunService = ReturnType<typeof createSyncRunService>\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { findAndCountWithDecryption, findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { SyncCursor, SyncRun } from '../data/entities'\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nfunction buildRunSearchFilter(search: string): FilterQuery<SyncRun>[] | null {\n const trimmed = search.trim()\n if (!trimmed) return null\n const pattern = `%${escapeLikePattern(trimmed)}%`\n const conditions: FilterQuery<SyncRun>[] = [\n { integrationId: { $ilike: pattern } },\n { entityType: { $ilike: pattern } },\n { status: { $ilike: pattern } },\n ]\n if (UUID_PATTERN.test(trimmed)) {\n conditions.push({ id: trimmed })\n }\n return conditions\n}\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n}\n\nexport type CursorCommitOptions = {\n /**\n * Mirror the committed cursor into the shared `sync_cursors` row. Defaults to\n * `true`; the engine passes the adapter's `persistsSharedCursor(entityType)`\n * verdict. `false` keeps the cursor on the run row alone.\n */\n persistSharedCursor?: boolean\n /**\n * Fences the write against a concurrent delivery: the run must still be\n * `running` and still sit on this batch count, or the commit throws\n * {@link SyncRunOwnershipConflictError} and rolls back. Omit to keep the\n * unguarded write for callers outside the engine.\n */\n expectedBatchesCompleted?: number\n}\n\n/** {@link CursorCommitOptions} minus the fence, which `updateCursor` does not apply. */\nexport type SharedCursorOption = Pick<CursorCommitOptions, 'persistSharedCursor'>\n\n/**\n * Raised when a batch commit loses the ownership compare-and-swap, meaning\n * another delivery of the same job advanced the run while this worker was\n * streaming.\n *\n * BullMQ guarantees at-least-once delivery: a job whose lock is not renewed is\n * redelivered under the SAME job id, whether the previous worker died or is only\n * blocked. No identity token can tell those apart, so ownership is enforced here\n * \u2014 on the write that matters \u2014 instead of at claim time. The loser aborts and\n * leaves the run to the worker that is still making progress.\n */\nexport class SyncRunOwnershipConflictError extends Error {\n constructor(\n readonly runId: string,\n readonly expectedBatchesCompleted: number,\n ) {\n super(`[internal] Sync run ${runId} advanced past batch ${expectedBatchesCompleted} under a concurrent worker`)\n this.name = 'SyncRunOwnershipConflictError'\n }\n}\n\nexport function createSyncRunService(em: EntityManager) {\n async function resolveCursorRow(run: SyncRun, scope: SyncScope): Promise<SyncCursor | null> {\n return findOneWithDecryption(\n em,\n SyncCursor,\n {\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n }\n\n function applyCursorMutation(\n run: SyncRun,\n cursorRow: SyncCursor | null,\n cursor: string,\n scope: SyncScope,\n persistSharedCursor: boolean,\n ): void {\n run.cursor = cursor\n if (!persistSharedCursor) return\n if (cursorRow) {\n cursorRow.cursor = cursor\n } else {\n em.create(SyncCursor, {\n integrationId: run.integrationId,\n entityType: run.entityType,\n direction: run.direction,\n cursor,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n }\n }\n\n return {\n async createRun(input: {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n progressJobId?: string | null\n jobId?: string | null\n parameters?: Record<string, unknown> | null\n }, scope: SyncScope): Promise<SyncRun> {\n const row = em.create(SyncRun, {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n status: 'pending',\n cursor: input.cursor,\n initialCursor: input.cursor,\n triggeredBy: input.triggeredBy,\n progressJobId: input.progressJobId,\n jobId: input.jobId,\n parameters: input.parameters ?? null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n await em.persist(row).flush()\n return row\n },\n\n async getRun(runId: string, scope: SyncScope): Promise<SyncRun | null> {\n return findOneWithDecryption(\n em,\n SyncRun,\n {\n id: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async listRuns(query: {\n integrationId?: string\n entityType?: string\n direction?: 'import' | 'export'\n status?: string\n search?: string\n page: number\n pageSize: number\n }, scope: SyncScope): Promise<{ items: SyncRun[]; total: number }> {\n const where: FilterQuery<SyncRun> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (query.integrationId) where.integrationId = query.integrationId\n if (query.entityType) where.entityType = query.entityType\n if (query.direction) where.direction = query.direction\n if (query.status) where.status = query.status as SyncRun['status']\n if (query.search) {\n const searchConditions = buildRunSearchFilter(query.search)\n if (searchConditions) where.$or = searchConditions\n }\n\n const [items, total] = await findAndCountWithDecryption(\n em,\n SyncRun,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n scope,\n )\n\n return { items, total }\n },\n\n async markStatus(runId: string, status: SyncRun['status'], scope: SyncScope, error?: string): Promise<SyncRun | null> {\n if (status === 'running') {\n const updated = await em.nativeUpdate(\n SyncRun,\n {\n id: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n // A BullMQ stalled-job redelivery finds the run in `running` after\n // the previous worker was hard-killed. Treat that transition as an\n // idempotent claim while still excluding terminal states so a\n // cancelled or completed run cannot be revived.\n status: { $in: ['pending', 'running'] },\n },\n {\n status,\n ...(error !== undefined ? { lastError: error } : {}),\n updatedAt: new Date(),\n },\n )\n if (updated === 0) return null\n const row = await this.getRun(runId, scope)\n if (row && typeof em.refresh === 'function') {\n await em.refresh(row)\n }\n return row\n }\n\n const row = await this.getRun(runId, scope)\n if (!row) return null\n const isTerminal = row.status === 'completed' || row.status === 'failed' || row.status === 'cancelled'\n if (isTerminal && row.status !== status) {\n return row\n }\n row.status = status\n if (error !== undefined) row.lastError = error\n await em.flush()\n return row\n },\n\n /**\n * @deprecated Use {@link commitBatchProgress}, which writes counters and\n * cursor in one transaction behind the ownership fence. This method updates\n * counters unfenced, so two deliveries of the same job can lose each other's\n * increments. Kept for external callers only.\n */\n async updateCounts(\n runId: string,\n delta: Partial<Pick<SyncRun, 'createdCount' | 'updatedCount' | 'skippedCount' | 'failedCount' | 'batchesCompleted'>>,\n scope: SyncScope,\n ): Promise<SyncRun | null> {\n const row = await this.getRun(runId, scope)\n if (!row) return null\n\n row.createdCount += delta.createdCount ?? 0\n row.updatedCount += delta.updatedCount ?? 0\n row.skippedCount += delta.skippedCount ?? 0\n row.failedCount += delta.failedCount ?? 0\n row.batchesCompleted += delta.batchesCompleted ?? 0\n await em.flush()\n return row\n },\n\n /**\n * @deprecated Use {@link commitBatchProgress}. This method advances the\n * cursor without the ownership fence, so a stale delivery can move the\n * cursor of a run another worker owns. Kept for external callers only.\n *\n * It still takes `persistSharedCursor` despite being deprecated: an external\n * caller advancing the cursor of an opted-out entity type would otherwise\n * create the very `sync_cursors` row the opt-out exists to avoid, and a\n * later incremental run would read it as a start position. The deprecated\n * path has to honour the opt-out for as long as it exists.\n */\n async updateCursor(runId: string, cursor: string, scope: SyncScope, options?: SharedCursorOption): Promise<void> {\n const run = await this.getRun(runId, scope)\n if (!run) return\n const persistSharedCursor = options?.persistSharedCursor ?? true\n const cursorRow = persistSharedCursor ? await resolveCursorRow(run, scope) : null\n await withAtomicFlush(em, [\n () => applyCursorMutation(run, cursorRow, cursor, scope, persistSharedCursor),\n ], { transaction: true })\n },\n\n /**\n * Commits one batch's counters and cursor in a single transaction.\n *\n * Passing `options.expectedBatchesCompleted` fences the write: the run must\n * still be `running` and still sit on that batch count, or another delivery\n * of the same BullMQ job owns the run and this commit throws\n * `SyncRunOwnershipConflictError` and rolls back. Omitting it keeps the\n * legacy unguarded write for callers outside the engine.\n *\n * `options.persistSharedCursor` is orthogonal to the fence: it decides\n * whether the committed cursor is mirrored into the shared `sync_cursors`\n * row, and the two compose freely \u2014 a fenced commit for an opted-out entity\n * type advances the run row alone and still throws on a stale fence.\n *\n * The fence token is `batchesCompleted` rather than `cursor` because it\n * advances by construction on every commit. A cursor is a free-form adapter\n * string that an adapter may legitimately repeat between batches \u2014 the\n * Akeneo products adapter does, between its final page and the\n * reconciliation batch that follows it \u2014 and a repeated token fences\n * nothing.\n *\n * The guard's `UPDATE` also holds the row lock for the rest of the\n * transaction, so a competing commit blocks here and then re-reads the\n * advanced count instead of interleaving with this one. That is what lets\n * the counters below stay a plain read-modify-write against the snapshot\n * read above: a commit that wins the fence has proven that nothing else\n * landed since it read.\n */\n async commitBatchProgress(\n runId: string,\n delta: Partial<Pick<SyncRun, 'createdCount' | 'updatedCount' | 'skippedCount' | 'failedCount' | 'batchesCompleted'>>,\n cursor: string,\n scope: SyncScope,\n options?: CursorCommitOptions,\n ): Promise<SyncRun | null> {\n const run = await this.getRun(runId, scope)\n if (!run) return null\n const { expectedBatchesCompleted } = options ?? {}\n const persistSharedCursor = options?.persistSharedCursor ?? true\n const cursorRow = persistSharedCursor ? await resolveCursorRow(run, scope) : null\n const claimRunOwnership = async () => {\n if ((delta.batchesCompleted ?? 0) < 1) {\n throw new Error(`[internal] A fenced commit for sync run ${runId} must advance batchesCompleted`)\n }\n const owned = await em.nativeUpdate(\n SyncRun,\n {\n id: runId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n status: 'running',\n batchesCompleted: expectedBatchesCompleted,\n },\n { updatedAt: new Date() },\n )\n if (owned === 0) {\n throw new SyncRunOwnershipConflictError(runId, expectedBatchesCompleted ?? 0)\n }\n }\n await withAtomicFlush(em, [\n ...(expectedBatchesCompleted === undefined ? [] : [claimRunOwnership]),\n () => {\n run.createdCount += delta.createdCount ?? 0\n run.updatedCount += delta.updatedCount ?? 0\n run.skippedCount += delta.skippedCount ?? 0\n run.failedCount += delta.failedCount ?? 0\n run.batchesCompleted += delta.batchesCompleted ?? 0\n applyCursorMutation(run, cursorRow, cursor, scope, persistSharedCursor)\n },\n ], { transaction: true })\n return run\n },\n\n async resolveCursor(integrationId: string, entityType: string, direction: 'import' | 'export', scope: SyncScope): Promise<string | null> {\n const row = await findOneWithDecryption(\n em,\n SyncCursor,\n {\n integrationId,\n entityType,\n direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n return row?.cursor ?? null\n },\n\n /**\n * Resume position for an entity type whose adapter opted out of the shared\n * `sync_cursors` row: the cursor of the most recent run, unless that run\n * reached `completed`. A finished walk resumes from `null` so the next run\n * starts over rather than skipping everything an older interrupted run had\n * already passed.\n */\n async resolveResumeCursor(integrationId: string, entityType: string, direction: 'import' | 'export', scope: SyncScope): Promise<string | null> {\n const [run] = await findWithDecryption(\n em,\n SyncRun,\n {\n integrationId,\n entityType,\n direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'DESC' }, limit: 1 },\n scope,\n )\n if (!run || run.status === 'completed') return null\n return run.cursor ?? null\n },\n\n /**\n * Clears the run-scoped resume position for an entity type, so the next\n * non-`fullSync` run starts from the beginning. Returns how many runs were\n * cleared.\n *\n * This is the opt-out's equivalent of deleting the shared `sync_cursors`\n * row. An entity type whose adapter returns `false` from\n * `persistsSharedCursor` has no such row, so a reset flow that only deletes\n * `SyncCursor` would leave {@link resolveResumeCursor} returning the cursor\n * of the interrupted run it just reset against \u2014 re-importing the tail of a\n * walk instead of the whole thing. Reset flows MUST call this alongside\n * their `SyncCursor` delete; it is a no-op when nothing is interrupted.\n *\n * The `status` filter here selects which rows to clear. It is deliberately\n * NOT the read-side filter {@link resolveResumeCursor} avoids: that method\n * reads the single most recent run whatever its status, precisely so an\n * older interrupted run cannot outlive a later completed walk. Clearing\n * every interrupted run is enough to start fresh either way \u2014 if the latest\n * run was interrupted its cursor is now null, and if it completed the resume\n * path already returns null.\n */\n async resetResumePosition(\n integrationId: string,\n entityType: string,\n direction: 'import' | 'export',\n scope: SyncScope,\n ): Promise<number> {\n return em.nativeUpdate(\n SyncRun,\n {\n integrationId,\n entityType,\n direction,\n status: { $ne: 'completed' },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { cursor: null, updatedAt: new Date() },\n )\n },\n\n async findRunningOverlap(integrationId: string, entityType: string, direction: 'import' | 'export', scope: SyncScope): Promise<SyncRun | null> {\n const [run] = await findWithDecryption(\n em,\n SyncRun,\n {\n integrationId,\n entityType,\n direction,\n status: { $in: ['pending', 'running'] },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { limit: 1 },\n scope,\n )\n return run ?? null\n },\n }\n}\n\nexport type SyncRunService = ReturnType<typeof createSyncRunService>\n"],
5
+ "mappings": "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",
6
6
  "names": ["row"]
7
7
  }
@@ -0,0 +1,13 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260810120000 extends Migration {
3
+ async up() {
4
+ this.addSql(`alter table "sync_runs" add column "parameters" jsonb null;`);
5
+ }
6
+ async down() {
7
+ this.addSql(`alter table "sync_runs" drop column "parameters";`);
8
+ }
9
+ }
10
+ export {
11
+ Migration20260810120000
12
+ };
13
+ //# sourceMappingURL=Migration20260810120000.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/data_sync/migrations/Migration20260810120000.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260810120000 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`alter table \"sync_runs\" add column \"parameters\" jsonb null;`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`alter table \"sync_runs\" drop column \"parameters\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,6DAA6D;AAAA,EAC3E;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,mDAAmD;AAAA,EACjE;AAEF;",
6
+ "names": []
7
+ }
@@ -2,6 +2,9 @@ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find"
2
2
  import { SyncSchedule } from "../data/entities.js";
3
3
  import { startDataSyncRun } from "../lib/start-run.js";
4
4
  import { resolveAdapterForIntegration, resolveStartCursor } from "../lib/start-cursor.js";
5
+ import { normalizeRunParameters } from "../lib/run-parameters.js";
6
+ import { createLogger } from "@open-mercato/shared/lib/logger";
7
+ const logger = createLogger("data_sync").child({ component: "sync-scheduled" });
5
8
  const metadata = {
6
9
  queue: "data-sync-scheduled",
7
10
  id: "data-sync:scheduled",
@@ -40,14 +43,31 @@ async function handle(job, ctx) {
40
43
  if (overlap) {
41
44
  return;
42
45
  }
46
+ const adapter = resolveAdapterForIntegration(schedule.integrationId);
43
47
  const cursor = schedule.fullSync ? null : await resolveStartCursor({
44
48
  syncRunService,
45
- adapter: resolveAdapterForIntegration(schedule.integrationId),
49
+ adapter,
46
50
  integrationId: schedule.integrationId,
47
51
  entityType: schedule.entityType,
48
52
  direction: schedule.direction,
49
53
  scope: job.payload.scope
50
54
  });
55
+ const normalizedParameters = normalizeRunParameters(
56
+ adapter?.runParameters,
57
+ schedule.direction,
58
+ null,
59
+ schedule.entityType
60
+ );
61
+ if (!normalizedParameters.ok) {
62
+ logger.error("Scheduled run skipped: adapter declares invalid run parameter defaults", {
63
+ scheduleId: schedule.id,
64
+ integrationId: schedule.integrationId,
65
+ entityType: schedule.entityType,
66
+ keys: normalizedParameters.errors.map((error) => error.key)
67
+ });
68
+ return;
69
+ }
70
+ const parameters = Object.keys(normalizedParameters.values).length > 0 ? normalizedParameters.values : null;
51
71
  schedule.lastRunAt = /* @__PURE__ */ new Date();
52
72
  await em.flush();
53
73
  await startDataSyncRun({
@@ -59,7 +79,8 @@ async function handle(job, ctx) {
59
79
  entityType: schedule.entityType,
60
80
  direction: schedule.direction,
61
81
  cursor,
62
- triggeredBy: "scheduler"
82
+ triggeredBy: "scheduler",
83
+ parameters
63
84
  }
64
85
  });
65
86
  }