@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
|
@@ -14,6 +14,7 @@ export const last_error = "last_error";
|
|
|
14
14
|
export const progress_job_id = "progress_job_id";
|
|
15
15
|
export const job_id = "job_id";
|
|
16
16
|
export const triggered_by = "triggered_by";
|
|
17
|
+
export const parameters = "parameters";
|
|
17
18
|
export const organization_id = "organization_id";
|
|
18
19
|
export const tenant_id = "tenant_id";
|
|
19
20
|
export const created_at = "created_at";
|
|
@@ -3096,6 +3096,7 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
|
3096
3096
|
"progress_job_id": "progress_job_id",
|
|
3097
3097
|
"job_id": "job_id",
|
|
3098
3098
|
"triggered_by": "triggered_by",
|
|
3099
|
+
"parameters": "parameters",
|
|
3099
3100
|
"organization_id": "organization_id",
|
|
3100
3101
|
"tenant_id": "tenant_id",
|
|
3101
3102
|
"created_at": "created_at",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
258
|
+
"@open-mercato/shared": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
259
|
+
"@open-mercato/ui": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
265
|
+
"@open-mercato/shared": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
266
|
+
"@open-mercato/ui": "0.6.8-develop.6998.1.d2bc46c56c",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^7.0.0",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
sidebarPreferencesScopeSchema,
|
|
10
10
|
} from '../../../data/validators'
|
|
11
11
|
import {
|
|
12
|
+
findSidebarPreference,
|
|
12
13
|
loadRoleSidebarPreferenceUpdatedAt,
|
|
13
14
|
loadRoleSidebarPreferences,
|
|
14
|
-
loadSidebarPreference,
|
|
15
15
|
loadSidebarPreferenceUpdatedAt,
|
|
16
16
|
saveRoleSidebarPreference,
|
|
17
17
|
saveSidebarPreference,
|
|
@@ -204,7 +204,7 @@ export async function GET(req: Request) {
|
|
|
204
204
|
// For API key auth, use userId (the actual user) if available
|
|
205
205
|
const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub
|
|
206
206
|
const settings = effectiveUserId
|
|
207
|
-
? await
|
|
207
|
+
? await findSidebarPreference(em, {
|
|
208
208
|
userId: effectiveUserId,
|
|
209
209
|
tenantId: auth.tenantId ?? null,
|
|
210
210
|
organizationId: auth.orgId ?? null,
|
|
@@ -33,8 +33,8 @@ import { Role } from '@open-mercato/core/modules/auth/data/entities'
|
|
|
33
33
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
34
34
|
import {
|
|
35
35
|
applySidebarPreference,
|
|
36
|
+
findSidebarPreference,
|
|
36
37
|
loadFirstRoleSidebarPreference,
|
|
37
|
-
loadSidebarPreference,
|
|
38
38
|
} from '@open-mercato/core/modules/auth/services/sidebarPreferencesService'
|
|
39
39
|
import type { SidebarPreferencesSettings } from '@open-mercato/shared/modules/navigation/sidebarPreferences'
|
|
40
40
|
|
|
@@ -436,7 +436,7 @@ export async function resolveBackendChromePayload({
|
|
|
436
436
|
|
|
437
437
|
const effectiveUserId = auth.isApiKey ? auth.userId : auth.sub
|
|
438
438
|
if (effectiveUserId) {
|
|
439
|
-
userPreference = await
|
|
439
|
+
userPreference = await findSidebarPreference(em, {
|
|
440
440
|
userId: effectiveUserId,
|
|
441
441
|
tenantId: scopedTenantId,
|
|
442
442
|
organizationId: scopedOrganizationId,
|
|
@@ -37,10 +37,10 @@ export type SidebarGroupLike<T = Record<string, unknown>> = {
|
|
|
37
37
|
weight?: number
|
|
38
38
|
} & T
|
|
39
39
|
|
|
40
|
-
export async function
|
|
40
|
+
export async function findSidebarPreference(
|
|
41
41
|
em: EntityManager,
|
|
42
42
|
scope: SidebarPreferenceScope,
|
|
43
|
-
): Promise<SidebarPreferencesSettings> {
|
|
43
|
+
): Promise<SidebarPreferencesSettings | null> {
|
|
44
44
|
// Cross-locale: variants & preferences are scoped per (user, tenant, org) only.
|
|
45
45
|
// The `locale` field on the row is kept for audit / when the row was created.
|
|
46
46
|
const { userId, tenantId, organizationId } = normalizeScope(scope)
|
|
@@ -51,7 +51,26 @@ export async function loadSidebarPreference(
|
|
|
51
51
|
undefined,
|
|
52
52
|
{ tenantId, organizationId },
|
|
53
53
|
)
|
|
54
|
-
|
|
54
|
+
// `null` means "no saved preference" and is distinct from "a preference exists but is empty":
|
|
55
|
+
// callers layer role defaults beneath the user layout and must skip the user pass entirely,
|
|
56
|
+
// because applying empty settings overwrites the role state instead of merging with it.
|
|
57
|
+
if (!existing) return null
|
|
58
|
+
return normalizeSidebarSettings(existing.settingsJson as SidebarPreferencesSettings | null | undefined)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @deprecated Since 0.7.1, slated for removal in 0.9.0. Use `findSidebarPreference` and handle
|
|
63
|
+
* `null`. This function fabricates a default settings object for a user with no saved row, which
|
|
64
|
+
* is indistinguishable from a saved-but-empty preference — applying that result erases any role
|
|
65
|
+
* layer underneath it, because `applySidebarPreference` overwrites `hidden` rather than merging.
|
|
66
|
+
* Migration: `(await findSidebarPreference(em, scope)) ?? normalizeSidebarSettings(null)`
|
|
67
|
+
* reproduces this exact return value if you genuinely want the defaults.
|
|
68
|
+
*/
|
|
69
|
+
export async function loadSidebarPreference(
|
|
70
|
+
em: EntityManager,
|
|
71
|
+
scope: SidebarPreferenceScope,
|
|
72
|
+
): Promise<SidebarPreferencesSettings> {
|
|
73
|
+
return (await findSidebarPreference(em, scope)) ?? normalizeSidebarSettings(null)
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
export async function loadSidebarPreferenceUpdatedAt(
|
|
@@ -54,6 +54,7 @@ packages/core/src/modules/data_sync/
|
|
|
54
54
|
│ ├── adapter-registry.ts # Register/get adapters by providerKey
|
|
55
55
|
│ ├── id-mapping.ts # External ID ↔ local ID lookup and storage
|
|
56
56
|
│ ├── queue.ts # Queue helper for enqueuing sync jobs
|
|
57
|
+
│ ├── run-parameters.ts # Validate/coerce operator run parameters vs. adapter declaration
|
|
57
58
|
│ ├── sync-engine.ts # Orchestrates streaming import/export with progress
|
|
58
59
|
│ └── sync-run-service.ts # CRUD for SyncRun + cursor management
|
|
59
60
|
├── api/
|
|
@@ -97,23 +98,97 @@ Provider modules implement `DataSyncAdapter`:
|
|
|
97
98
|
|
|
98
99
|
```typescript
|
|
99
100
|
interface DataSyncAdapter {
|
|
100
|
-
providerKey: string
|
|
101
|
-
direction: 'import' | 'export' | 'bidirectional'
|
|
102
|
-
supportedEntities: string[]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
readonly providerKey: string
|
|
102
|
+
readonly direction: 'import' | 'export' | 'bidirectional'
|
|
103
|
+
readonly supportedEntities: string[]
|
|
104
|
+
readonly runMode?: 'generic' | 'provider'
|
|
105
|
+
readonly operationalTelemetry?: boolean
|
|
106
|
+
readonly runParameters?: RunParameter[]
|
|
107
|
+
|
|
108
|
+
streamImport?(input: StreamImportInput): AsyncIterable<ImportBatch>
|
|
109
|
+
streamExport?(input: StreamExportInput): AsyncIterable<ExportBatch>
|
|
110
|
+
getInitialCursor?(input: { entityType: string; scope: TenantScope }): Promise<string | null>
|
|
111
|
+
getMapping(input: { entityType: string; scope: TenantScope }): Promise<DataMapping>
|
|
107
112
|
persistsSharedCursor?(entityType: string): boolean
|
|
108
|
-
validateConnection?(
|
|
113
|
+
validateConnection?(input: {
|
|
114
|
+
entityType: string
|
|
115
|
+
credentials: Record<string, unknown>
|
|
116
|
+
mapping: DataMapping
|
|
117
|
+
scope: TenantScope
|
|
118
|
+
}): Promise<ValidationResult>
|
|
109
119
|
}
|
|
110
120
|
```
|
|
111
121
|
|
|
122
|
+
All hooks take a single input object — see `lib/adapter.ts` for the authoritative
|
|
123
|
+
shapes.
|
|
124
|
+
|
|
112
125
|
Register adapters in your provider module's `di.ts`:
|
|
113
126
|
```typescript
|
|
114
127
|
registerDataSyncAdapter(myAdapter)
|
|
115
128
|
```
|
|
116
129
|
|
|
130
|
+
### Run parameters
|
|
131
|
+
|
|
132
|
+
Adapters may declare optional, operator-facing `runParameters`. The dashboard
|
|
133
|
+
renders a generic input per declared parameter, the run API validates and
|
|
134
|
+
coerces the submitted values against the declaration (`lib/run-parameters.ts`),
|
|
135
|
+
and the normalized values are persisted on the run and passed back on
|
|
136
|
+
`StreamImportInput.parameters` / `StreamExportInput.parameters`. Keep
|
|
137
|
+
declarations provider-agnostic — never special-case a provider in `data_sync`.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
runParameters: [
|
|
141
|
+
{ key: 'dryRun', label: 'Dry run', type: 'boolean', defaultValue: false,
|
|
142
|
+
description: 'Report what would change without writing.' },
|
|
143
|
+
{ key: 'startId', label: 'Start id', type: 'number', min: 0 },
|
|
144
|
+
{ key: 'mode', label: 'Mode', type: 'select',
|
|
145
|
+
options: [{ value: 'fast' }, { value: 'thorough' }] },
|
|
146
|
+
// Only offered when the orders entity is selected:
|
|
147
|
+
{ key: 'bulk', label: 'Bulk reindex', type: 'boolean', entityType: 'sales_orders' },
|
|
148
|
+
]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Supported types: `boolean`, `string`, `number`, `select`. A parameter may set
|
|
152
|
+
`direction` to apply to only `import` or `export` runs, and `entityType`
|
|
153
|
+
(a `supportedEntities` value or an array of them) to apply only when that
|
|
154
|
+
entity is selected — use it when a knob only makes sense for one entity's run.
|
|
155
|
+
Params without `direction` / `entityType` apply to every run. Blank values fall
|
|
156
|
+
back to `defaultValue`; values are retained across retries.
|
|
157
|
+
|
|
158
|
+
Run parameters are **operator-visible and stored in clear text** on
|
|
159
|
+
`sync_runs.parameters`, and rendered read-only on the run detail page. Never
|
|
160
|
+
declare a parameter that carries a secret — credentials belong in
|
|
161
|
+
`integrationCredentialsService`.
|
|
162
|
+
|
|
163
|
+
**Translation.** `label`, `description` and `placeholder` are literals, so an
|
|
164
|
+
adapter shipping to more than one locale MUST also set `labelKey` /
|
|
165
|
+
`descriptionKey` / `placeholderKey`; the dashboard prefers the key and falls
|
|
166
|
+
back to the literal. Validation failures come back from the API as
|
|
167
|
+
`{ key, code, params, message }` — the UI renders `code` through
|
|
168
|
+
`data_sync.runParameters.errors.<code>` and only uses the English `message` as
|
|
169
|
+
a fallback, so never rely on the sentence text.
|
|
170
|
+
|
|
171
|
+
**Retry re-validates.** `POST /api/data_sync/runs/[id]/retry` re-runs the stored
|
|
172
|
+
values through `normalizeRunParameters` against the *current* declaration:
|
|
173
|
+
parameters you have since removed or re-scoped fall away, and a value that no
|
|
174
|
+
longer satisfies its declaration fails the retry with a 422 instead of reaching
|
|
175
|
+
the adapter. An adapter therefore never receives a set the run API would reject
|
|
176
|
+
today — tighten a bound freely.
|
|
177
|
+
|
|
178
|
+
The integration detail page's schedule table also starts runs, but has no room
|
|
179
|
+
for a parameter form: it submits the declared defaults and refuses the run when
|
|
180
|
+
an applicable parameter is `required` with no `defaultValue`, pointing the
|
|
181
|
+
operator at the Data Sync dashboard. Declare a `defaultValue` for anything that
|
|
182
|
+
should stay launchable from that table.
|
|
183
|
+
|
|
184
|
+
**Recurring runs get your defaults, not operator values.** A `SyncSchedule`
|
|
185
|
+
cannot yet pin a chosen value, but the scheduled worker normalizes an empty
|
|
186
|
+
input against your declaration, so a scheduled run hands you the same set an
|
|
187
|
+
untouched dashboard form would — never an empty object. Write your adapter
|
|
188
|
+
against the defaults, not against `undefined`. A default that violates its own
|
|
189
|
+
declaration skips the scheduled run with a logged error instead of starting it
|
|
190
|
+
with a half-applied set.
|
|
191
|
+
|
|
117
192
|
If the sync provider needs bootstrap credentials, mappings, locales, channels, or other default sync settings after a fresh install, implement a provider-owned env preset flow:
|
|
118
193
|
|
|
119
194
|
- read env vars in the provider package
|
|
@@ -55,6 +55,7 @@ export async function GET(req: Request) {
|
|
|
55
55
|
runMode: adapter.runMode ?? 'generic',
|
|
56
56
|
canStartRun: adapter.runMode !== 'provider',
|
|
57
57
|
supportedEntities: adapter.supportedEntities,
|
|
58
|
+
runParameters: adapter.runParameters ?? [],
|
|
58
59
|
hasCredentials: Boolean(credentials),
|
|
59
60
|
isEnabled,
|
|
60
61
|
settingsPath: `/backend/integrations/${encodeURIComponent(integration.id)}`,
|
|
@@ -10,6 +10,7 @@ import type { SyncRunService } from '../lib/sync-run-service'
|
|
|
10
10
|
import { runSyncSchema } from '../data/validators'
|
|
11
11
|
import { startDataSyncRun } from '../lib/start-run'
|
|
12
12
|
import { getDataSyncAdapter } from '../lib/adapter-registry'
|
|
13
|
+
import { normalizeRunParameters } from '../lib/run-parameters'
|
|
13
14
|
import { resolveStartCursor } from '../lib/start-cursor'
|
|
14
15
|
import {
|
|
15
16
|
runCrudMutationGuardAfterSuccess,
|
|
@@ -78,6 +79,19 @@ export async function POST(req: Request) {
|
|
|
78
79
|
return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
const normalizedParameters = normalizeRunParameters(
|
|
83
|
+
adapter.runParameters,
|
|
84
|
+
parsed.data.direction,
|
|
85
|
+
parsed.data.parameters,
|
|
86
|
+
parsed.data.entityType,
|
|
87
|
+
)
|
|
88
|
+
if (!normalizedParameters.ok) {
|
|
89
|
+
return NextResponse.json(
|
|
90
|
+
{ error: 'Invalid run parameters', details: { parameters: normalizedParameters.errors } },
|
|
91
|
+
{ status: 422 },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)
|
|
82
96
|
if (!integrationEnabled) {
|
|
83
97
|
return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })
|
|
@@ -133,6 +147,9 @@ export async function POST(req: Request) {
|
|
|
133
147
|
cursor,
|
|
134
148
|
triggeredBy: parsed.data.triggeredBy ?? auth.sub,
|
|
135
149
|
batchSize: parsed.data.batchSize,
|
|
150
|
+
parameters: Object.keys(normalizedParameters.values).length > 0
|
|
151
|
+
? normalizedParameters.values
|
|
152
|
+
: null,
|
|
136
153
|
},
|
|
137
154
|
})
|
|
138
155
|
|
|
@@ -8,6 +8,7 @@ import type { ProgressService } from '../../../../progress/lib/progressService'
|
|
|
8
8
|
import type { SyncRunService } from '../../../lib/sync-run-service'
|
|
9
9
|
import { retrySyncSchema } from '../../../data/validators'
|
|
10
10
|
import { startDataSyncRun } from '../../../lib/start-run'
|
|
11
|
+
import { normalizeRunParameters } from '../../../lib/run-parameters'
|
|
11
12
|
import { resolveAdapterForIntegration, resolveStartCursor } from '../../../lib/start-cursor'
|
|
12
13
|
import {
|
|
13
14
|
runCrudMutationGuardAfterSuccess,
|
|
@@ -88,11 +89,40 @@ export async function POST(req: Request, ctx: { params?: Promise<{ id?: string }
|
|
|
88
89
|
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// A retry replays the stored parameters, but the adapter's declaration may
|
|
93
|
+
// have moved on since the original run — a parameter dropped, a bound
|
|
94
|
+
// tightened, a select option removed, a scope narrowed. Re-normalize against
|
|
95
|
+
// the current declaration so an adapter never receives a set the run API
|
|
96
|
+
// would reject today. Undeclared keys fall away silently; values that are now
|
|
97
|
+
// invalid stop the retry, because the operator has no form here to fix them.
|
|
98
|
+
const retryAdapter = resolveAdapterForIntegration(previous.integrationId)
|
|
99
|
+
const normalizedParameters = normalizeRunParameters(
|
|
100
|
+
retryAdapter?.runParameters,
|
|
101
|
+
previous.direction,
|
|
102
|
+
previous.parameters ?? null,
|
|
103
|
+
previous.entityType,
|
|
104
|
+
)
|
|
105
|
+
if (!normalizedParameters.ok) {
|
|
106
|
+
return NextResponse.json(
|
|
107
|
+
{
|
|
108
|
+
error: 'Stored run parameters are no longer valid for this integration. Start a new run from the Data Sync dashboard.',
|
|
109
|
+
// Machine-readable so the dashboard can render the way out in the
|
|
110
|
+
// operator's language; the English sentence stays for non-UI callers.
|
|
111
|
+
code: 'parametersStale',
|
|
112
|
+
details: { parameters: normalizedParameters.errors },
|
|
113
|
+
},
|
|
114
|
+
{ status: 422 },
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
const retryParameters = Object.keys(normalizedParameters.values).length > 0
|
|
118
|
+
? normalizedParameters.values
|
|
119
|
+
: null
|
|
120
|
+
|
|
91
121
|
const cursor = parsedBody.data.fromBeginning
|
|
92
122
|
? null
|
|
93
123
|
: previous.cursor ?? await resolveStartCursor({
|
|
94
124
|
syncRunService,
|
|
95
|
-
adapter:
|
|
125
|
+
adapter: retryAdapter,
|
|
96
126
|
integrationId: previous.integrationId,
|
|
97
127
|
entityType: previous.entityType,
|
|
98
128
|
direction: previous.direction,
|
|
@@ -113,6 +143,7 @@ export async function POST(req: Request, ctx: { params?: Promise<{ id?: string }
|
|
|
113
143
|
cursor,
|
|
114
144
|
triggeredBy: auth.sub,
|
|
115
145
|
batchSize: 100,
|
|
146
|
+
parameters: retryParameters,
|
|
116
147
|
progressJob: {
|
|
117
148
|
name: `Retry data sync ${previous.integrationId} — ${previous.entityType}`,
|
|
118
149
|
},
|
|
@@ -69,6 +69,7 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
69
69
|
batchesCompleted: run.batchesCompleted,
|
|
70
70
|
lastError: run.lastError ?? null,
|
|
71
71
|
progressJobId: run.progressJobId ?? null,
|
|
72
|
+
parameters: run.parameters ?? null,
|
|
72
73
|
progressJob: progressJob
|
|
73
74
|
? {
|
|
74
75
|
id: progressJob.id,
|
|
@@ -44,6 +44,18 @@ import {
|
|
|
44
44
|
ShieldCheck,
|
|
45
45
|
} from 'lucide-react'
|
|
46
46
|
import { getSyncRunStatusVariant, getSyncSummaryVariant } from '../../lib/syncRunStatus'
|
|
47
|
+
import type { RunParameter } from '../../lib/adapter'
|
|
48
|
+
import { getApplicableRunParameters } from '../../lib/run-parameters'
|
|
49
|
+
import {
|
|
50
|
+
RunParameterFields,
|
|
51
|
+
buildDefaultRunParameterValues,
|
|
52
|
+
buildRetryFailureMessage,
|
|
53
|
+
buildRunFailureMessage,
|
|
54
|
+
buildRunParametersPayload,
|
|
55
|
+
type RetryFailureBody,
|
|
56
|
+
type RunFailureBody,
|
|
57
|
+
type RunParameterFormValue,
|
|
58
|
+
} from '../../components/RunParameterFields'
|
|
47
59
|
|
|
48
60
|
type SyncRunRow = {
|
|
49
61
|
id: string
|
|
@@ -73,6 +85,7 @@ type SyncOption = {
|
|
|
73
85
|
runMode?: 'generic' | 'provider'
|
|
74
86
|
canStartRun?: boolean
|
|
75
87
|
supportedEntities: string[]
|
|
88
|
+
runParameters?: RunParameter[]
|
|
76
89
|
hasCredentials: boolean
|
|
77
90
|
isEnabled: boolean
|
|
78
91
|
settingsPath: string
|
|
@@ -149,6 +162,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
149
162
|
const [selectedDirection, setSelectedDirection] = React.useState<'import' | 'export'>('import')
|
|
150
163
|
const [batchSize, setBatchSize] = React.useState('100')
|
|
151
164
|
const [fullSync, setFullSync] = React.useState(false)
|
|
165
|
+
const [paramValues, setParamValues] = React.useState<Record<string, RunParameterFormValue>>({})
|
|
152
166
|
const [scheduleEditor, setScheduleEditor] = React.useState<SyncScheduleEditorState>(() => buildDefaultScheduleState(''))
|
|
153
167
|
const [isLoadingSchedule, setIsLoadingSchedule] = React.useState(false)
|
|
154
168
|
const [isSavingSchedule, setIsSavingSchedule] = React.useState(false)
|
|
@@ -231,6 +245,27 @@ export default function SyncRunsDashboardPage() {
|
|
|
231
245
|
[selectedIntegration],
|
|
232
246
|
)
|
|
233
247
|
|
|
248
|
+
const runParameters = React.useMemo(
|
|
249
|
+
() => getApplicableRunParameters(
|
|
250
|
+
selectedIntegration?.runParameters,
|
|
251
|
+
selectedDirection,
|
|
252
|
+
// Pass the state through as-is. Before an entity is chosen it is '',
|
|
253
|
+
// which matches no `entityType` and so hides scoped parameters — the
|
|
254
|
+
// wanted outcome. Mapping '' to `undefined` would mean "skip entity
|
|
255
|
+
// scoping" and show every scoped parameter instead.
|
|
256
|
+
selectedEntityType,
|
|
257
|
+
),
|
|
258
|
+
[selectedIntegration, selectedDirection, selectedEntityType],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
setParamValues(buildDefaultRunParameterValues(runParameters))
|
|
263
|
+
}, [runParameters])
|
|
264
|
+
|
|
265
|
+
const updateParamValue = React.useCallback((key: string, value: RunParameterFormValue) => {
|
|
266
|
+
setParamValues((current) => ({ ...current, [key]: value }))
|
|
267
|
+
}, [])
|
|
268
|
+
|
|
234
269
|
React.useEffect(() => {
|
|
235
270
|
if (!selectedIntegration) {
|
|
236
271
|
setSelectedEntityType('')
|
|
@@ -325,7 +360,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
325
360
|
flash(t('data_sync.runs.detail.retrySuccess'), 'success')
|
|
326
361
|
setReloadToken((token) => token + 1)
|
|
327
362
|
} else {
|
|
328
|
-
flash(
|
|
363
|
+
flash(buildRetryFailureMessage(call.result as RetryFailureBody | null, t), 'error')
|
|
329
364
|
}
|
|
330
365
|
}, [t])
|
|
331
366
|
|
|
@@ -352,27 +387,25 @@ export default function SyncRunsDashboardPage() {
|
|
|
352
387
|
return
|
|
353
388
|
}
|
|
354
389
|
|
|
390
|
+
const parameters = buildRunParametersPayload(runParameters, paramValues)
|
|
391
|
+
const requestBody: Record<string, unknown> = {
|
|
392
|
+
integrationId: selectedIntegration.integrationId,
|
|
393
|
+
entityType: selectedEntityType,
|
|
394
|
+
direction: selectedDirection,
|
|
395
|
+
batchSize: parsedBatchSize,
|
|
396
|
+
fullSync,
|
|
397
|
+
}
|
|
398
|
+
if (runParameters.length > 0) requestBody.parameters = parameters
|
|
399
|
+
|
|
355
400
|
try {
|
|
356
401
|
const call = await runMutation({
|
|
357
402
|
// optimistic-lock-exempt: starts a new sync run (create), not a concurrent record edit
|
|
358
403
|
operation: () => apiCall<{ id: string }>('/api/data_sync/run', {
|
|
359
404
|
method: 'POST',
|
|
360
405
|
headers: { 'Content-Type': 'application/json' },
|
|
361
|
-
body: JSON.stringify(
|
|
362
|
-
integrationId: selectedIntegration.integrationId,
|
|
363
|
-
entityType: selectedEntityType,
|
|
364
|
-
direction: selectedDirection,
|
|
365
|
-
batchSize: parsedBatchSize,
|
|
366
|
-
fullSync,
|
|
367
|
-
}),
|
|
406
|
+
body: JSON.stringify(requestBody),
|
|
368
407
|
}, { fallback: null }),
|
|
369
|
-
mutationPayload:
|
|
370
|
-
integrationId: selectedIntegration.integrationId,
|
|
371
|
-
entityType: selectedEntityType,
|
|
372
|
-
direction: selectedDirection,
|
|
373
|
-
batchSize: parsedBatchSize,
|
|
374
|
-
fullSync,
|
|
375
|
-
},
|
|
408
|
+
mutationPayload: requestBody,
|
|
376
409
|
context: {
|
|
377
410
|
operation: 'create',
|
|
378
411
|
actionId: 'start-sync-run',
|
|
@@ -381,7 +414,11 @@ export default function SyncRunsDashboardPage() {
|
|
|
381
414
|
})
|
|
382
415
|
|
|
383
416
|
if (!call.ok || !call.result?.id) {
|
|
384
|
-
flash((
|
|
417
|
+
flash(buildRunFailureMessage(
|
|
418
|
+
call.result as RunFailureBody | null,
|
|
419
|
+
t('data_sync.dashboard.start.error', 'Failed to start sync run'),
|
|
420
|
+
t,
|
|
421
|
+
), 'error')
|
|
385
422
|
return
|
|
386
423
|
}
|
|
387
424
|
|
|
@@ -392,7 +429,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
392
429
|
const message = error instanceof Error ? error.message : t('data_sync.dashboard.start.error', 'Failed to start sync run')
|
|
393
430
|
flash(message, 'error')
|
|
394
431
|
}
|
|
395
|
-
}, [batchSize, fullSync, router, runMutation, selectedDirection, selectedEntityType, selectedIntegration, t])
|
|
432
|
+
}, [batchSize, fullSync, paramValues, router, runMutation, runParameters, selectedDirection, selectedEntityType, selectedIntegration, t])
|
|
396
433
|
|
|
397
434
|
const handleSaveSchedule = React.useCallback(async () => {
|
|
398
435
|
if (!selectedIntegration || !selectedEntityType) return
|
|
@@ -784,6 +821,25 @@ export default function SyncRunsDashboardPage() {
|
|
|
784
821
|
</div>
|
|
785
822
|
</div>
|
|
786
823
|
|
|
824
|
+
{runParameters.length > 0 ? (
|
|
825
|
+
<div className="mt-4 space-y-3">
|
|
826
|
+
<div className="flex items-center gap-2">
|
|
827
|
+
<Settings2 className="size-4 text-muted-foreground" />
|
|
828
|
+
<h4 className="text-sm font-semibold">
|
|
829
|
+
{t('data_sync.dashboard.start.parameters', 'Run parameters')}
|
|
830
|
+
</h4>
|
|
831
|
+
</div>
|
|
832
|
+
<p className="text-xs text-muted-foreground">
|
|
833
|
+
{t('data_sync.dashboard.start.parametersHelp', 'Optional values this integration accepts for the manual run.')}
|
|
834
|
+
</p>
|
|
835
|
+
<RunParameterFields
|
|
836
|
+
params={runParameters}
|
|
837
|
+
values={paramValues}
|
|
838
|
+
onChange={updateParamValue}
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
) : null}
|
|
842
|
+
|
|
787
843
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
|
788
844
|
<p className="text-xs text-muted-foreground">
|
|
789
845
|
{t('data_sync.dashboard.start.runNowFootnote', 'Manual runs show progress immediately and land on the run detail page after launch.')}
|
|
@@ -19,6 +19,17 @@ import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato
|
|
|
19
19
|
import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
20
20
|
import { RotateCcw, XCircle } from 'lucide-react'
|
|
21
21
|
import { getSyncRunStatusVariant } from '../../../../lib/syncRunStatus'
|
|
22
|
+
import {
|
|
23
|
+
buildRetryFailureMessage,
|
|
24
|
+
resolveRunParameterText,
|
|
25
|
+
type RetryFailureBody,
|
|
26
|
+
} from '../../../../components/RunParameterFields'
|
|
27
|
+
|
|
28
|
+
type RunParameterDeclaration = {
|
|
29
|
+
key: string
|
|
30
|
+
label?: string
|
|
31
|
+
labelKey?: string
|
|
32
|
+
}
|
|
22
33
|
|
|
23
34
|
type SyncRunDetail = {
|
|
24
35
|
id: string
|
|
@@ -33,6 +44,7 @@ type SyncRunDetail = {
|
|
|
33
44
|
batchesCompleted: number
|
|
34
45
|
lastError: string | null
|
|
35
46
|
progressJobId: string | null
|
|
47
|
+
parameters: Record<string, unknown> | null
|
|
36
48
|
progressJob: {
|
|
37
49
|
id: string
|
|
38
50
|
status: string
|
|
@@ -111,6 +123,11 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
111
123
|
const [logsTotal, setLogsTotal] = React.useState(0)
|
|
112
124
|
const [logsPage, setLogsPage] = React.useState(1)
|
|
113
125
|
const logsPageRef = React.useRef(1)
|
|
126
|
+
const [parameterLabels, setParameterLabels] = React.useState<Record<string, string>>({})
|
|
127
|
+
// Declarations cannot change between two refreshes of the same run, so the
|
|
128
|
+
// options list is fetched once per integration rather than on every progress
|
|
129
|
+
// event that re-reads the run.
|
|
130
|
+
const parameterLabelsIntegrationRef = React.useRef<string | null>(null)
|
|
114
131
|
|
|
115
132
|
const resolveCurrentRunId = React.useCallback(() => {
|
|
116
133
|
return runId ?? (
|
|
@@ -120,6 +137,26 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
120
137
|
)
|
|
121
138
|
}, [runId])
|
|
122
139
|
|
|
140
|
+
// The run row stores machine keys. Resolve the adapter's declared labels so a
|
|
141
|
+
// past run reads as "Start id" rather than "startId"; keys the adapter no
|
|
142
|
+
// longer declares keep their raw form, which keeps historical runs readable.
|
|
143
|
+
const loadParameterLabels = React.useCallback(async (integrationId: string) => {
|
|
144
|
+
if (parameterLabelsIntegrationRef.current === integrationId) return
|
|
145
|
+
parameterLabelsIntegrationRef.current = integrationId
|
|
146
|
+
const call = await apiCall<{ items?: Array<{ integrationId: string; runParameters?: RunParameterDeclaration[] }> }>(
|
|
147
|
+
'/api/data_sync/options',
|
|
148
|
+
undefined,
|
|
149
|
+
{ fallback: { items: [] } },
|
|
150
|
+
)
|
|
151
|
+
const declared = (call.result?.items ?? []).find((item) => item.integrationId === integrationId)?.runParameters ?? []
|
|
152
|
+
const labels: Record<string, string> = {}
|
|
153
|
+
for (const param of declared) {
|
|
154
|
+
const resolved = resolveRunParameterText(t, param.labelKey, param.label)
|
|
155
|
+
if (resolved) labels[param.key] = resolved
|
|
156
|
+
}
|
|
157
|
+
setParameterLabels(labels)
|
|
158
|
+
}, [t])
|
|
159
|
+
|
|
123
160
|
const loadRun = React.useCallback(async () => {
|
|
124
161
|
const currentRunId = resolveCurrentRunId()
|
|
125
162
|
if (!currentRunId) {
|
|
@@ -144,7 +181,10 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
144
181
|
}
|
|
145
182
|
setRun(call.result)
|
|
146
183
|
setIsLoading(false)
|
|
147
|
-
|
|
184
|
+
if (call.result.parameters && Object.keys(call.result.parameters).length > 0) {
|
|
185
|
+
void loadParameterLabels(call.result.integrationId)
|
|
186
|
+
}
|
|
187
|
+
}, [loadParameterLabels, resolveCurrentRunId, t])
|
|
148
188
|
|
|
149
189
|
const loadLogs = React.useCallback(async (page?: number) => {
|
|
150
190
|
const currentRunId = resolveCurrentRunId()
|
|
@@ -268,7 +308,7 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
268
308
|
flash(t('data_sync.runs.detail.retrySuccess'), 'success')
|
|
269
309
|
router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)
|
|
270
310
|
} else {
|
|
271
|
-
flash(
|
|
311
|
+
flash(buildRetryFailureMessage(call.result as RetryFailureBody | null, t), 'error')
|
|
272
312
|
}
|
|
273
313
|
}, [resolveCurrentRunId, router, runMutation, t])
|
|
274
314
|
|
|
@@ -404,6 +444,26 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
404
444
|
</Card>
|
|
405
445
|
</div>
|
|
406
446
|
|
|
447
|
+
{run.parameters && Object.keys(run.parameters).length > 0 ? (
|
|
448
|
+
<Card>
|
|
449
|
+
<CardHeader>
|
|
450
|
+
<CardTitle>{t('data_sync.runs.detail.parameters', 'Run parameters')}</CardTitle>
|
|
451
|
+
</CardHeader>
|
|
452
|
+
<CardContent>
|
|
453
|
+
<dl className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
454
|
+
{Object.entries(run.parameters).map(([key, value]) => (
|
|
455
|
+
<div key={key} className="flex items-center justify-between gap-3 rounded-md border bg-card px-3 py-2 text-sm">
|
|
456
|
+
<dt className="font-medium text-muted-foreground">{parameterLabels[key] ?? key}</dt>
|
|
457
|
+
<dd className="font-mono text-foreground">
|
|
458
|
+
{typeof value === 'boolean' ? String(value) : String(value ?? '')}
|
|
459
|
+
</dd>
|
|
460
|
+
</div>
|
|
461
|
+
))}
|
|
462
|
+
</dl>
|
|
463
|
+
</CardContent>
|
|
464
|
+
</Card>
|
|
465
|
+
) : null}
|
|
466
|
+
|
|
407
467
|
{run.lastError && (
|
|
408
468
|
<Card className="border-status-error-border bg-status-error-bg">
|
|
409
469
|
<CardHeader>
|