@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/sync_run/index.js +2 -0
- package/dist/generated/entities/sync_run/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js.map +1 -1
- package/dist/modules/auth/lib/backendChrome.js +3 -3
- package/dist/modules/auth/lib/backendChrome.js.map +1 -1
- package/dist/modules/auth/services/sidebarPreferencesService.js +7 -2
- package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +1 -0
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +15 -1
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +23 -1
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/route.js +1 -0
- package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +60 -17
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +34 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +21 -15
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/components/RunParameterFields.js +135 -0
- package/dist/modules/data_sync/components/RunParameterFields.js.map +7 -0
- package/dist/modules/data_sync/data/entities.js +3 -0
- package/dist/modules/data_sync/data/entities.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +4 -1
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/run-parameters.js +108 -0
- package/dist/modules/data_sync/lib/run-parameters.js.map +7 -0
- package/dist/modules/data_sync/lib/start-run.js +1 -0
- package/dist/modules/data_sync/lib/start-run.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +4 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +1 -0
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/migrations/Migration20260810120000.js +13 -0
- package/dist/modules/data_sync/migrations/Migration20260810120000.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-scheduled.js +23 -2
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/query_index/di.js +125 -48
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +39 -1
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +26 -5
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/generated/entities/sync_run/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/auth/api/sidebar/preferences/route.ts +2 -2
- package/src/modules/auth/lib/backendChrome.tsx +2 -2
- package/src/modules/auth/services/sidebarPreferencesService.ts +22 -3
- package/src/modules/data_sync/AGENTS.md +83 -8
- package/src/modules/data_sync/api/options.ts +1 -0
- package/src/modules/data_sync/api/run.ts +17 -0
- package/src/modules/data_sync/api/runs/[id]/retry.ts +32 -1
- package/src/modules/data_sync/api/runs/[id]/route.ts +1 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +73 -17
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +62 -2
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +28 -15
- package/src/modules/data_sync/components/RunParameterFields.tsx +229 -0
- package/src/modules/data_sync/data/entities.ts +4 -1
- package/src/modules/data_sync/data/validators.ts +3 -0
- package/src/modules/data_sync/i18n/de.json +10 -0
- package/src/modules/data_sync/i18n/en.json +10 -0
- package/src/modules/data_sync/i18n/es.json +10 -0
- package/src/modules/data_sync/i18n/ko.json +10 -0
- package/src/modules/data_sync/i18n/pl.json +10 -0
- package/src/modules/data_sync/lib/adapter.ts +88 -0
- package/src/modules/data_sync/lib/run-parameters.ts +196 -0
- package/src/modules/data_sync/lib/start-run.ts +2 -0
- package/src/modules/data_sync/lib/sync-engine.ts +5 -1
- package/src/modules/data_sync/lib/sync-run-service.ts +2 -0
- package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/data_sync/migrations/Migration20260810120000.ts +13 -0
- package/src/modules/data_sync/workers/sync-scheduled.ts +32 -1
- package/src/modules/query_index/di.ts +153 -56
- package/src/modules/sales/api/documents/factory.ts +62 -1
- package/src/modules/sales/commands/documents.ts +40 -4
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- 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
|
|
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
|
}
|