@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
@@ -38,6 +38,13 @@ export interface StreamImportInput {
38
38
  mapping: DataMapping
39
39
  scope: TenantScope
40
40
  runId?: string
41
+ /**
42
+ * Operator-supplied run parameters, normalized against the adapter's
43
+ * declared `runParameters`. Only declared keys are present; values are
44
+ * already coerced to the declared types. Empty object when the adapter
45
+ * declares no parameters.
46
+ */
47
+ parameters?: Record<string, RunParameterValue>
41
48
  }
42
49
 
43
50
  export interface ImportItem {
@@ -67,6 +74,8 @@ export interface StreamExportInput {
67
74
  scope: TenantScope
68
75
  filter?: Record<string, unknown>
69
76
  runId?: string
77
+ /** See {@link StreamImportInput.parameters}. */
78
+ parameters?: Record<string, RunParameterValue>
70
79
  }
71
80
 
72
81
  export interface ExportItemResult {
@@ -89,6 +98,79 @@ export interface ValidationResult {
89
98
  details?: Record<string, unknown>
90
99
  }
91
100
 
101
+ export type RunParameterType = 'boolean' | 'string' | 'number' | 'select'
102
+
103
+ export type RunParameterValue = boolean | string | number
104
+
105
+ export interface RunParameterOption {
106
+ value: string
107
+ label?: string
108
+ }
109
+
110
+ /**
111
+ * A single operator-facing parameter an adapter accepts when a run is started.
112
+ *
113
+ * The `data_sync` UI renders an input per declared parameter, the run API
114
+ * validates and coerces the submitted values against this declaration, and the
115
+ * normalized values are handed back to the adapter on `StreamImportInput` /
116
+ * `StreamExportInput`. Keep declarations generic — this contract is provider
117
+ * agnostic.
118
+ */
119
+ export interface RunParameter {
120
+ /** Stable identifier used as the key in the normalized parameters object. */
121
+ key: string
122
+ /**
123
+ * Human-readable label shown next to the input. Used verbatim when
124
+ * `labelKey` is absent, and as the fallback when it resolves to nothing.
125
+ */
126
+ label: string
127
+ /**
128
+ * i18n key the dashboard resolves for the label, preferred over `label`.
129
+ * Declare it whenever the adapter ships to more than one locale — `label`
130
+ * alone can only ever be one language.
131
+ */
132
+ labelKey?: string
133
+ type: RunParameterType
134
+ /** Optional helper text rendered under the input. */
135
+ description?: string
136
+ /** i18n key for `description`, preferred over the literal when present. */
137
+ descriptionKey?: string
138
+ /**
139
+ * When true, the run cannot start unless a non-empty value is provided.
140
+ *
141
+ * Has no effect on `boolean` parameters: a switch always submits `true` or
142
+ * `false`, neither of which is blank, so the check can never fire. Use
143
+ * `defaultValue` to express the intended default instead. This is why the
144
+ * dashboard renders the required marker only on the input types where the
145
+ * constraint is real.
146
+ */
147
+ required?: boolean
148
+ /** Pre-filled value; also the value used when the field is left blank. */
149
+ defaultValue?: RunParameterValue
150
+ /** Placeholder for `string` / `number` inputs. */
151
+ placeholder?: string
152
+ /** i18n key for `placeholder`, preferred over the literal when present. */
153
+ placeholderKey?: string
154
+ /** Allowed choices for `select` parameters. */
155
+ options?: RunParameterOption[]
156
+ /** Inclusive bounds for `number` parameters. */
157
+ min?: number
158
+ max?: number
159
+ /**
160
+ * Restrict the parameter to a single run direction. When omitted the
161
+ * parameter applies to both import and export runs.
162
+ */
163
+ direction?: 'import' | 'export'
164
+ /**
165
+ * Restrict the parameter to one or more entity types (matched against the
166
+ * run's `entityType`, i.e. a value from `supportedEntities`). When omitted
167
+ * the parameter applies to every entity the adapter supports. Use this when
168
+ * a knob only makes sense for a specific entity's run — e.g. a bulk-reindex
169
+ * toggle that only applies to the orders backfill.
170
+ */
171
+ entityType?: string | string[]
172
+ }
173
+
92
174
  export interface DataSyncAdapter {
93
175
  readonly providerKey: string
94
176
  readonly direction: 'import' | 'export' | 'bidirectional'
@@ -103,6 +185,12 @@ export interface DataSyncAdapter {
103
185
  */
104
186
  readonly runMode?: 'generic' | 'provider'
105
187
  readonly operationalTelemetry?: boolean
188
+ /**
189
+ * Optional operator-facing parameters accepted when a run is started. The
190
+ * `data_sync` dashboard renders an input per entry, and the normalized values
191
+ * are passed back on `StreamImportInput` / `StreamExportInput`.
192
+ */
193
+ readonly runParameters?: RunParameter[]
106
194
 
107
195
  /**
108
196
  * Batch work MUST be replay-safe.
@@ -0,0 +1,196 @@
1
+ import type { RunParameter, RunParameterValue } from './adapter'
2
+
3
+ export type RunParameterErrorCode = 'required' | 'type' | 'min' | 'max' | 'select'
4
+
5
+ export type RunParameterError = {
6
+ key: string
7
+ /**
8
+ * Machine-readable reason, so the client can render a translated message.
9
+ * The dashboard maps these to `data_sync.runParameters.errors.<code>`.
10
+ */
11
+ code: RunParameterErrorCode
12
+ /** Values the translated message interpolates (label, bound, option list). */
13
+ params: { label: string; type?: string; min?: number; max?: number; options?: string }
14
+ /**
15
+ * Pre-rendered English sentence. Kept for non-UI callers (API clients, logs)
16
+ * and as the client-side fallback; the UI prefers `code` + `params`.
17
+ */
18
+ message: string
19
+ }
20
+
21
+ export type NormalizeRunParametersResult =
22
+ | { ok: true; values: Record<string, RunParameterValue> }
23
+ | { ok: false; errors: RunParameterError[] }
24
+
25
+ /**
26
+ * Keys that cannot round-trip through a plain object literal. Assigning a
27
+ * primitive to `__proto__` is silently discarded rather than creating an own
28
+ * property, so a parameter declared under that key would vanish between the
29
+ * form and the adapter instead of failing loudly. Rejecting it here — the one
30
+ * place every surface funnels through — keeps the dashboard, the default-value
31
+ * builder and the normalizer agreeing on the same parameter set.
32
+ *
33
+ * (`constructor` / `prototype` assign normally and are left alone.)
34
+ */
35
+ const RESERVED_PARAMETER_KEYS = new Set(['__proto__'])
36
+
37
+ export function isReservedRunParameterKey(key: string): boolean {
38
+ return RESERVED_PARAMETER_KEYS.has(key)
39
+ }
40
+
41
+ /**
42
+ * Returns the declared parameters that apply to a given run. A parameter
43
+ * without an explicit `direction` applies to both directions; one without an
44
+ * explicit `entityType` applies to every entity. When `entityType` is omitted
45
+ * here (the caller does not know the run's entity), entity scoping is skipped.
46
+ * Parameters declared under a reserved key are dropped.
47
+ */
48
+ export function getApplicableRunParameters(
49
+ declared: RunParameter[] | undefined,
50
+ direction: 'import' | 'export',
51
+ entityType?: string,
52
+ ): RunParameter[] {
53
+ if (!declared || declared.length === 0) return []
54
+ return declared.filter((param) => {
55
+ if (isReservedRunParameterKey(param.key)) return false
56
+ if (param.direction && param.direction !== direction) return false
57
+ if (param.entityType !== undefined && entityType !== undefined) {
58
+ const allowed = Array.isArray(param.entityType) ? param.entityType : [param.entityType]
59
+ if (!allowed.includes(entityType)) return false
60
+ }
61
+ return true
62
+ })
63
+ }
64
+
65
+ function isBlank(value: unknown): boolean {
66
+ return value === undefined || value === null || (typeof value === 'string' && value.trim().length === 0)
67
+ }
68
+
69
+ function coerceBoolean(value: unknown): boolean | null {
70
+ if (typeof value === 'boolean') return value
71
+ if (typeof value === 'number') {
72
+ if (value === 1) return true
73
+ if (value === 0) return false
74
+ return null
75
+ }
76
+ if (typeof value === 'string') {
77
+ const normalized = value.trim().toLowerCase()
78
+ if (['true', '1', 'yes', 'on'].includes(normalized)) return true
79
+ if (['false', '0', 'no', 'off'].includes(normalized)) return false
80
+ }
81
+ return null
82
+ }
83
+
84
+ function coerceNumber(value: unknown): number | null {
85
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null
86
+ if (typeof value === 'string') {
87
+ const trimmed = value.trim()
88
+ if (trimmed.length === 0) return null
89
+ const parsed = Number(trimmed)
90
+ return Number.isFinite(parsed) ? parsed : null
91
+ }
92
+ return null
93
+ }
94
+
95
+ /**
96
+ * Validate and coerce an untrusted parameter object against an adapter's
97
+ * declared `runParameters` for the given run direction and entity type.
98
+ *
99
+ * - Parameters that do not apply to the direction or entity type are ignored.
100
+ * - Undeclared keys in the input are dropped (never passed through).
101
+ * - Blank values fall back to `defaultValue`; a blank required value is an error.
102
+ * - Values are coerced to the declared type; the result only contains declared keys.
103
+ */
104
+ function buildRunParameterError(
105
+ param: RunParameter,
106
+ code: RunParameterErrorCode,
107
+ extra: { type?: string; min?: number; max?: number; options?: string } = {},
108
+ ): RunParameterError {
109
+ const message = code === 'required' ? `${param.label} is required.`
110
+ : code === 'type' ? `${param.label} must be a ${extra.type}.`
111
+ : code === 'min' ? `${param.label} must be at least ${extra.min}.`
112
+ : code === 'max' ? `${param.label} must be at most ${extra.max}.`
113
+ : `${param.label} must be one of: ${extra.options}.`
114
+ return { key: param.key, code, params: { label: param.label, ...extra }, message }
115
+ }
116
+
117
+ type CoerceResult =
118
+ | { ok: true; value: RunParameterValue }
119
+ | { ok: false; error: RunParameterError }
120
+
121
+ /** Coerces and range-checks one non-blank value against its declaration. */
122
+ function coerceDeclaredValue(param: RunParameter, raw: unknown): CoerceResult {
123
+ switch (param.type) {
124
+ case 'boolean': {
125
+ const coerced = coerceBoolean(raw)
126
+ if (coerced === null) return { ok: false, error: buildRunParameterError(param, 'type', { type: 'boolean' }) }
127
+ return { ok: true, value: coerced }
128
+ }
129
+ case 'number': {
130
+ const coerced = coerceNumber(raw)
131
+ if (coerced === null) return { ok: false, error: buildRunParameterError(param, 'type', { type: 'number' }) }
132
+ if (typeof param.min === 'number' && coerced < param.min) {
133
+ return { ok: false, error: buildRunParameterError(param, 'min', { min: param.min }) }
134
+ }
135
+ if (typeof param.max === 'number' && coerced > param.max) {
136
+ return { ok: false, error: buildRunParameterError(param, 'max', { max: param.max }) }
137
+ }
138
+ return { ok: true, value: coerced }
139
+ }
140
+ case 'select': {
141
+ const candidate = String(raw)
142
+ const allowed = (param.options ?? []).map((option) => option.value)
143
+ if (!allowed.includes(candidate)) {
144
+ return { ok: false, error: buildRunParameterError(param, 'select', { options: allowed.join(', ') }) }
145
+ }
146
+ return { ok: true, value: candidate }
147
+ }
148
+ case 'string':
149
+ default:
150
+ return { ok: true, value: String(raw).trim() }
151
+ }
152
+ }
153
+
154
+ export function normalizeRunParameters(
155
+ declared: RunParameter[] | undefined,
156
+ direction: 'import' | 'export',
157
+ raw: Record<string, unknown> | null | undefined,
158
+ entityType?: string,
159
+ ): NormalizeRunParametersResult {
160
+ const params = getApplicableRunParameters(declared, direction, entityType)
161
+ const input = raw && typeof raw === 'object' ? raw : {}
162
+ const values: Record<string, RunParameterValue> = {}
163
+ const errors: RunParameterError[] = []
164
+
165
+ for (const param of params) {
166
+ // Own-property lookup only: a parameter declared under an inherited name
167
+ // (`constructor`, `toString`, `valueOf`) would otherwise read back the
168
+ // inherited function instead of `undefined`, so a required check would
169
+ // silently pass and a string parameter would coerce the function source.
170
+ const provided = Object.prototype.hasOwnProperty.call(input, param.key)
171
+ ? (input as Record<string, unknown>)[param.key]
172
+ : undefined
173
+
174
+ if (isBlank(provided)) {
175
+ if (param.defaultValue === undefined) {
176
+ if (param.required) errors.push(buildRunParameterError(param, 'required'))
177
+ continue
178
+ }
179
+ // The declared default goes through the same checks as a submitted
180
+ // value, so a misdeclared adapter (a `select` default outside its own
181
+ // options, a number below its own `min`) fails loudly here rather than
182
+ // shipping an invalid value to itself mid-run.
183
+ const fallback = coerceDeclaredValue(param, param.defaultValue)
184
+ if (fallback.ok) values[param.key] = fallback.value
185
+ else errors.push(fallback.error)
186
+ continue
187
+ }
188
+
189
+ const coerced = coerceDeclaredValue(param, provided)
190
+ if (coerced.ok) values[param.key] = coerced.value
191
+ else errors.push(coerced.error)
192
+ }
193
+
194
+ if (errors.length > 0) return { ok: false, errors }
195
+ return { ok: true, values }
196
+ }
@@ -16,6 +16,7 @@ export type StartDataSyncRunInput = {
16
16
  cursor?: string | null
17
17
  triggeredBy?: string | null
18
18
  batchSize?: number
19
+ parameters?: Record<string, unknown> | null
19
20
  createProgressJob?: boolean
20
21
  progressJob?: {
21
22
  jobType?: string
@@ -64,6 +65,7 @@ export async function startDataSyncRun(params: {
64
65
  direction: input.direction,
65
66
  cursor: input.cursor ?? null,
66
67
  triggeredBy: input.triggeredBy ?? scope.userId ?? null,
68
+ parameters: input.parameters ?? null,
67
69
  progressJobId: progressJob?.id ?? null,
68
70
  },
69
71
  {
@@ -7,7 +7,7 @@ import type { ProgressService } from '../../progress/lib/progressService'
7
7
  import { STALE_JOB_TIMEOUT_SECONDS } from '../../progress/lib/progressService'
8
8
  import { refreshCoverageSnapshot } from '../../query_index/lib/coverage'
9
9
  import { emitDataSyncEvent } from '../events'
10
- import type { DataSyncAdapter, DataMapping, ExportBatch, ImportBatch } from './adapter'
10
+ import type { DataSyncAdapter, DataMapping, ExportBatch, ImportBatch, RunParameterValue } from './adapter'
11
11
  import { getDataSyncAdapter, resolveProviderKey } from './adapter-registry'
12
12
  import type { SyncRunService } from './sync-run-service'
13
13
  import { SyncRunOwnershipConflictError } from './sync-run-service'
@@ -15,6 +15,8 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
15
15
 
16
16
  const logger = createLogger('data_sync').child({ component: 'sync-engine' })
17
17
 
18
+ type RunParameters = Record<string, RunParameterValue>
19
+
18
20
  type SyncScope = {
19
21
  organizationId: string
20
22
  tenantId: string
@@ -550,6 +552,7 @@ export function createSyncEngine(deps: EngineDeps) {
550
552
  mapping,
551
553
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
552
554
  runId: run.id,
555
+ parameters: (run.parameters ?? {}) as RunParameters,
553
556
  }),
554
557
  makeHeartbeatTick(run.progressJobId, scope),
555
558
  HEARTBEAT_TICK_MS,
@@ -714,6 +717,7 @@ export function createSyncEngine(deps: EngineDeps) {
714
717
  mapping,
715
718
  scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
716
719
  runId: run.id,
720
+ parameters: (run.parameters ?? {}) as RunParameters,
717
721
  }),
718
722
  makeHeartbeatTick(run.progressJobId, scope),
719
723
  HEARTBEAT_TICK_MS,
@@ -115,6 +115,7 @@ export function createSyncRunService(em: EntityManager) {
115
115
  triggeredBy?: string | null
116
116
  progressJobId?: string | null
117
117
  jobId?: string | null
118
+ parameters?: Record<string, unknown> | null
118
119
  }, scope: SyncScope): Promise<SyncRun> {
119
120
  const row = em.create(SyncRun, {
120
121
  integrationId: input.integrationId,
@@ -126,6 +127,7 @@ export function createSyncRunService(em: EntityManager) {
126
127
  triggeredBy: input.triggeredBy,
127
128
  progressJobId: input.progressJobId,
128
129
  jobId: input.jobId,
130
+ parameters: input.parameters ?? null,
129
131
  organizationId: scope.organizationId,
130
132
  tenantId: scope.tenantId,
131
133
  })
@@ -668,6 +668,22 @@
668
668
  "comment": null,
669
669
  "enumItems": [],
670
670
  "mappedType": "integer"
671
+ },
672
+ "parameters": {
673
+ "name": "parameters",
674
+ "type": "jsonb",
675
+ "unsigned": false,
676
+ "autoincrement": false,
677
+ "primary": false,
678
+ "nullable": true,
679
+ "unique": false,
680
+ "length": null,
681
+ "precision": null,
682
+ "scale": null,
683
+ "default": null,
684
+ "comment": null,
685
+ "enumItems": [],
686
+ "mappedType": "json"
671
687
  }
672
688
  },
673
689
  "indexes": [
@@ -994,4 +1010,4 @@
994
1010
  ],
995
1011
  "views": [],
996
1012
  "nativeEnums": {}
997
- }
1013
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260810120000 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "sync_runs" add column "parameters" jsonb null;`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "sync_runs" drop column "parameters";`);
11
+ }
12
+
13
+ }
@@ -7,6 +7,10 @@ import type { SyncRunService } from '../lib/sync-run-service'
7
7
  import { SyncSchedule } from '../data/entities'
8
8
  import { startDataSyncRun } from '../lib/start-run'
9
9
  import { resolveAdapterForIntegration, resolveStartCursor } from '../lib/start-cursor'
10
+ import { normalizeRunParameters } from '../lib/run-parameters'
11
+ import { createLogger } from '@open-mercato/shared/lib/logger'
12
+
13
+ const logger = createLogger('data_sync').child({ component: 'sync-scheduled' })
10
14
 
11
15
  type ScheduledSyncPayload = {
12
16
  scheduleId: string
@@ -64,17 +68,43 @@ export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx:
64
68
  return
65
69
  }
66
70
 
71
+ const adapter = resolveAdapterForIntegration(schedule.integrationId)
72
+
67
73
  const cursor = schedule.fullSync
68
74
  ? null
69
75
  : await resolveStartCursor({
70
76
  syncRunService,
71
- adapter: resolveAdapterForIntegration(schedule.integrationId),
77
+ adapter,
72
78
  integrationId: schedule.integrationId,
73
79
  entityType: schedule.entityType,
74
80
  direction: schedule.direction,
75
81
  scope: job.payload.scope,
76
82
  })
77
83
 
84
+ // A schedule carries no parameter form, but the adapter's declared defaults
85
+ // still apply: normalizing an empty input materializes exactly those, so a
86
+ // scheduled run reaches the adapter with the same set a manual run would
87
+ // rather than an empty object. A declaration whose own default is invalid
88
+ // stops the run instead of handing over a half-applied set.
89
+ const normalizedParameters = normalizeRunParameters(
90
+ adapter?.runParameters,
91
+ schedule.direction,
92
+ null,
93
+ schedule.entityType,
94
+ )
95
+ if (!normalizedParameters.ok) {
96
+ logger.error('Scheduled run skipped: adapter declares invalid run parameter defaults', {
97
+ scheduleId: schedule.id,
98
+ integrationId: schedule.integrationId,
99
+ entityType: schedule.entityType,
100
+ keys: normalizedParameters.errors.map((error) => error.key),
101
+ })
102
+ return
103
+ }
104
+ const parameters = Object.keys(normalizedParameters.values).length > 0
105
+ ? normalizedParameters.values
106
+ : null
107
+
78
108
  schedule.lastRunAt = new Date()
79
109
  await em.flush()
80
110
 
@@ -88,6 +118,7 @@ export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx:
88
118
  direction: schedule.direction,
89
119
  cursor,
90
120
  triggeredBy: 'scheduler',
121
+ parameters,
91
122
  },
92
123
  })
93
124
  }