@open-mercato/core 0.6.3-develop.3588.1.2f62d1bb0d → 0.6.3-develop.3598.1.259c138c7d
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/helpers/integration/salesUi.js +24 -1
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -36
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
- package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
- package/dist/modules/data_sync/api/run.js +75 -65
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +26 -16
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/sync_excel/api/import/route.js +126 -116
- package/dist/modules/sync_excel/api/import/route.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/salesUi.ts +26 -1
- package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -50
- package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
- package/src/modules/data_sync/api/run.ts +80 -70
- package/src/modules/progress/api/jobs/route.ts +30 -20
- package/src/modules/sync_excel/api/import/route.ts +141 -131
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/sync_excel/api/import/route.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { z } from 'zod'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport type { SyncRunService } from '@open-mercato/core/modules/data_sync/lib/sync-run-service'\nimport { startDataSyncRun } from '@open-mercato/core/modules/data_sync/lib/start-run'\nimport { SyncMapping } from '@open-mercato/core/modules/data_sync/data/entities'\nimport type { CredentialsService } from '@open-mercato/core/modules/integrations/lib/credentials-service'\nimport type { IntegrationStateService } from '@open-mercato/core/modules/integrations/lib/state-service'\nimport { SyncExcelUpload } from '../../data/entities'\nimport { Attachment } from '../../../attachments/data/entities'\nimport { createCursor } from '../../lib/adapters/customers'\nimport {\n syncExcelImportRequestSchema,\n syncExcelImportResponseSchema,\n} from '../../data/validators'\nimport { resolveSyncExcelConcreteScope } from '../../lib/scope'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sync_excel.run'] },\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi = {\n tags: ['SyncExcel'],\n summary: 'Start a CSV import run for a stored sync_excel upload',\n methods: {\n POST: {\n summary: 'Start CSV import',\n responses: [\n { status: 201, description: 'Import run started', schema: syncExcelImportResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Upload not found', schema: errorSchema },\n { status: 409, description: 'Import overlap detected', schema: errorSchema },\n { status: 422, description: 'Invalid import payload', schema: errorSchema },\n ],\n },\n },\n}\n\nexport async function POST(request: Request) {\n const auth = await getAuthFromRequest(request)\n
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AACtC,SAAS,SAAS;AAGlB,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAG5B,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qCAAqC;AAEvC,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AACjE;AAEA,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,WAAW;AAAA,EAClB,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,8BAA8B;AAAA,QACxF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,oBAAoB,QAAQ,YAAY;AAAA,QACpE,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,YAAY;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,YAAY;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,KAAK,SAAkB;AAC3C,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { z } from 'zod'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport type { SyncRunService } from '@open-mercato/core/modules/data_sync/lib/sync-run-service'\nimport { startDataSyncRun } from '@open-mercato/core/modules/data_sync/lib/start-run'\nimport { SyncMapping } from '@open-mercato/core/modules/data_sync/data/entities'\nimport type { CredentialsService } from '@open-mercato/core/modules/integrations/lib/credentials-service'\nimport type { IntegrationStateService } from '@open-mercato/core/modules/integrations/lib/state-service'\nimport { SyncExcelUpload } from '../../data/entities'\nimport { Attachment } from '../../../attachments/data/entities'\nimport { createCursor } from '../../lib/adapters/customers'\nimport {\n syncExcelImportRequestSchema,\n syncExcelImportResponseSchema,\n} from '../../data/validators'\nimport { resolveSyncExcelConcreteScope } from '../../lib/scope'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sync_excel.run'] },\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi = {\n tags: ['SyncExcel'],\n summary: 'Start a CSV import run for a stored sync_excel upload',\n methods: {\n POST: {\n summary: 'Start CSV import',\n responses: [\n { status: 201, description: 'Import run started', schema: syncExcelImportResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 404, description: 'Upload not found', schema: errorSchema },\n { status: 409, description: 'Import overlap detected', schema: errorSchema },\n { status: 422, description: 'Invalid import payload', schema: errorSchema },\n ],\n },\n },\n}\n\nexport async function POST(request: Request) {\n try {\n const auth = await getAuthFromRequest(request)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const payload = await readJsonSafe(request)\n const parsedPayload = syncExcelImportRequestSchema.safeParse(payload)\n if (!parsedPayload.success) {\n return NextResponse.json({ error: 'Invalid import payload.' }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService\n const scopeResult = await resolveSyncExcelConcreteScope({ auth, container, request })\n if (!scopeResult.ok) {\n return NextResponse.json({ error: scopeResult.error }, { status: scopeResult.status })\n }\n const { scope } = scopeResult\n\n const upload = await findOneWithDecryption(\n em,\n SyncExcelUpload,\n {\n id: parsedPayload.data.uploadId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n if (!upload) {\n return NextResponse.json({ error: 'Upload preview not found.' }, { status: 404 })\n }\n\n const attachment = await findOneWithDecryption(\n em,\n Attachment,\n {\n id: upload.attachmentId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n if (!attachment) {\n return NextResponse.json({ error: 'Upload attachment not found.' }, { status: 404 })\n }\n\n if (upload.entityType !== parsedPayload.data.entityType) {\n return NextResponse.json({ error: 'Upload entity type does not match requested import target.' }, { status: 422 })\n }\n\n if (parsedPayload.data.mapping.entityType !== parsedPayload.data.entityType) {\n return NextResponse.json({ error: 'Mapping entity type does not match requested import target.' }, { status: 422 })\n }\n\n const overlap = await syncRunService.findRunningOverlap('sync_excel', parsedPayload.data.entityType, 'import', scope)\n if (overlap) {\n return NextResponse.json({ error: 'A sync_excel import is already in progress for this entity type.' }, { status: 409 })\n }\n\n const existingMapping = await findOneWithDecryption(\n em,\n SyncMapping,\n {\n integrationId: 'sync_excel',\n entityType: parsedPayload.data.entityType,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n if (existingMapping) {\n existingMapping.mapping = parsedPayload.data.mapping\n } else {\n em.persist(em.create(SyncMapping, {\n integrationId: 'sync_excel',\n entityType: parsedPayload.data.entityType,\n mapping: parsedPayload.data.mapping,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }))\n }\n\n await credentialsService.save('sync_excel', {}, scope)\n await integrationStateService.upsert('sync_excel', { isEnabled: true }, scope)\n await em.flush()\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: 'sync_excel',\n entityType: parsedPayload.data.entityType,\n direction: 'import',\n cursor: createCursor(upload.id, 0),\n triggeredBy: auth.sub,\n batchSize: parsedPayload.data.batchSize ?? 100,\n progressJob: {\n jobType: 'sync_excel:import',\n name: `CSV import \u2014 ${parsedPayload.data.entityType}`,\n description: upload.filename,\n meta: {\n integrationId: 'sync_excel',\n uploadId: upload.id,\n hiddenFromTopBar: false,\n },\n },\n },\n })\n\n upload.syncRunId = run.id\n upload.status = 'importing'\n await em.flush()\n\n return NextResponse.json(syncExcelImportResponseSchema.parse({\n runId: run.id,\n progressJobId: progressJob?.id ?? null,\n status: run.status,\n }), { status: 201 })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n const stack = error instanceof Error ? error.stack : undefined\n console.error('[sync_excel.import] unhandled error', { message, stack })\n return NextResponse.json(\n { error: 'Failed to start sync_excel import.', message },\n { status: 500 },\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3598.1.259c138c7d",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -242,16 +242,16 @@
|
|
|
242
242
|
"zod": "^4.4.3"
|
|
243
243
|
},
|
|
244
244
|
"peerDependencies": {
|
|
245
|
-
"@open-mercato/ai-assistant": "0.6.3-develop.
|
|
246
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
247
|
-
"@open-mercato/ui": "0.6.3-develop.
|
|
245
|
+
"@open-mercato/ai-assistant": "0.6.3-develop.3598.1.259c138c7d",
|
|
246
|
+
"@open-mercato/shared": "0.6.3-develop.3598.1.259c138c7d",
|
|
247
|
+
"@open-mercato/ui": "0.6.3-develop.3598.1.259c138c7d",
|
|
248
248
|
"react": "^19.0.0",
|
|
249
249
|
"react-dom": "^19.0.0"
|
|
250
250
|
},
|
|
251
251
|
"devDependencies": {
|
|
252
|
-
"@open-mercato/ai-assistant": "0.6.3-develop.
|
|
253
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
254
|
-
"@open-mercato/ui": "0.6.3-develop.
|
|
252
|
+
"@open-mercato/ai-assistant": "0.6.3-develop.3598.1.259c138c7d",
|
|
253
|
+
"@open-mercato/shared": "0.6.3-develop.3598.1.259c138c7d",
|
|
254
|
+
"@open-mercato/ui": "0.6.3-develop.3598.1.259c138c7d",
|
|
255
255
|
"@testing-library/dom": "^10.4.1",
|
|
256
256
|
"@testing-library/jest-dom": "^6.9.1",
|
|
257
257
|
"@testing-library/react": "^16.3.1",
|
|
@@ -872,7 +872,32 @@ export async function createSalesDocument(page: Page, options: CreateDocumentOpt
|
|
|
872
872
|
return id;
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
|
|
875
|
+
// The Create button can detach during the click as the React form re-renders
|
|
876
|
+
// after lookups settle. Retry briefly with a short stability wait, then fall
|
|
877
|
+
// back to the API fixture path if the click never lands.
|
|
878
|
+
let clicked = false;
|
|
879
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
880
|
+
try {
|
|
881
|
+
await page.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {});
|
|
882
|
+
const liveButton = page.getByRole('button', { name: /^Create$/i }).first();
|
|
883
|
+
if (!(await liveButton.isVisible().catch(() => false))) break;
|
|
884
|
+
if (!(await liveButton.isEnabled().catch(() => false))) {
|
|
885
|
+
await page.waitForTimeout(300);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
await liveButton.click({ timeout: 5_000 });
|
|
889
|
+
clicked = true;
|
|
890
|
+
break;
|
|
891
|
+
} catch {
|
|
892
|
+
await page.waitForTimeout(300);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (!clicked) {
|
|
896
|
+
const token = await resolveFallbackToken();
|
|
897
|
+
const id = await createSalesDocumentFixture(page, token, options.kind, customerQuery, channelQuery, fixtureContext);
|
|
898
|
+
await openSalesDocumentPage(page, id, options.kind);
|
|
899
|
+
return id;
|
|
900
|
+
}
|
|
876
901
|
const navigated = await page.waitForURL(
|
|
877
902
|
new RegExp(
|
|
878
903
|
`/backend/sales/(?:documents/[0-9a-f-]{36}\\?kind=${options.kind}|${options.kind === 'order' ? 'orders' : 'quotes'}/[0-9a-f-]{36})$`,
|
|
@@ -1,63 +1,46 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import { sql } from '@mikro-orm/postgresql'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// The previous implementation probed `information_schema.columns` on every
|
|
4
|
+
// first call via `em.getKysely()` to decide whether to add a `deletedAt: null`
|
|
5
|
+
// filter. When that probe ran inside an already-active MikroORM transaction it
|
|
6
|
+
// shared the transaction's connection — and any failure (including transient
|
|
7
|
+
// connection-pool state, an aborted TX from a sibling query, or pg returning
|
|
8
|
+
// 25P02) propagated back into the host transaction, poisoning every following
|
|
9
|
+
// query with `current transaction is aborted, commands ignored until end of
|
|
10
|
+
// transaction block`. That is what produced the standalone-CI failure burst
|
|
11
|
+
// (loadPersonCompanyLinks → 25P02 → customers.people.create → 500) and the
|
|
12
|
+
// sync_excel worker `failedCount: 1` regression.
|
|
13
|
+
//
|
|
14
|
+
// The `deleted_at` column on `customer_person_company_links` has been part of
|
|
15
|
+
// the committed module snapshot since `Migration20260415095203` (April 2026)
|
|
16
|
+
// and is referenced by `Migration20260417140000` (partial unique index uses
|
|
17
|
+
// `where deleted_at is null`). Any environment running these migrations
|
|
18
|
+
// already has the column. So the probe is no longer load-bearing — it only
|
|
19
|
+
// adds risk. We now unconditionally include `deletedAt: null` in the filter.
|
|
20
|
+
// If a downstream DB is genuinely un-migrated, the underlying `em.find` will
|
|
21
|
+
// surface the authentic `column "deleted_at" of relation
|
|
22
|
+
// "customer_person_company_links" does not exist` (pg 42703) pointing the
|
|
23
|
+
// operator at the real fix (`yarn db:migrate`).
|
|
24
|
+
//
|
|
25
|
+
// `customerPersonCompanyLinksSupportDeletedAt` and
|
|
26
|
+
// `warnMissingCustomerPersonCompanyLinksDeletedAt` are kept as exported
|
|
27
|
+
// no-op-equivalents so external callers (and the published @open-mercato/core
|
|
28
|
+
// API surface) don't break.
|
|
6
29
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function customerPersonCompanyLinksSupportDeletedAt(em: EntityManager): Promise<boolean> {
|
|
11
|
-
if (typeof (em as { getKysely?: unknown }).getKysely !== 'function') {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
if (supportsDeletedAtColumnPromise) return supportsDeletedAtColumnPromise
|
|
15
|
-
const probe = (async (): Promise<boolean> => {
|
|
16
|
-
try {
|
|
17
|
-
const db = em.getKysely<any>() as any
|
|
18
|
-
const row = await db
|
|
19
|
-
.selectFrom('information_schema.columns')
|
|
20
|
-
.select(['column_name'])
|
|
21
|
-
.where(sql<boolean>`table_schema = current_schema()`)
|
|
22
|
-
.where('table_name', '=', PERSON_COMPANY_LINKS_TABLE)
|
|
23
|
-
.where('column_name', '=', PERSON_COMPANY_LINKS_DELETED_AT_COLUMN)
|
|
24
|
-
.executeTakeFirst()
|
|
25
|
-
return !!row
|
|
26
|
-
} catch {
|
|
27
|
-
// The probe can fail transiently (e.g. when invoked inside an already-aborted
|
|
28
|
-
// postgres transaction — 25P02). Clear the cache so the next caller re-probes
|
|
29
|
-
// on a healthy connection, and assume the column is present: the migration is
|
|
30
|
-
// committed, and if it really is missing the downstream em.find will surface
|
|
31
|
-
// the authentic "column does not exist" error instead of a misleading warning.
|
|
32
|
-
supportsDeletedAtColumnPromise = null
|
|
33
|
-
return true
|
|
34
|
-
}
|
|
35
|
-
})()
|
|
36
|
-
supportsDeletedAtColumnPromise = probe
|
|
37
|
-
return probe
|
|
30
|
+
export async function customerPersonCompanyLinksSupportDeletedAt(_em: EntityManager): Promise<boolean> {
|
|
31
|
+
return true
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
export function warnMissingCustomerPersonCompanyLinksDeletedAt(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
warnedAboutMissingDeletedAtColumn = true
|
|
45
|
-
console.warn(
|
|
46
|
-
`[${source}] missing ${PERSON_COMPANY_LINKS_TABLE}.${PERSON_COMPANY_LINKS_DELETED_AT_COLUMN}; ` +
|
|
47
|
-
'continuing without link soft-delete filtering. Run yarn db:migrate.',
|
|
48
|
-
)
|
|
34
|
+
export function warnMissingCustomerPersonCompanyLinksDeletedAt(_source: string): void {
|
|
35
|
+
// No-op: column is guaranteed present by Migration20260415095203 and the
|
|
36
|
+
// module snapshot. Kept for backward-compatible exports.
|
|
49
37
|
}
|
|
50
38
|
|
|
51
39
|
export async function withActiveCustomerPersonCompanyLinkFilter<T extends Record<string, unknown>>(
|
|
52
|
-
|
|
40
|
+
_em: EntityManager,
|
|
53
41
|
where: T,
|
|
54
|
-
|
|
42
|
+
_source: string,
|
|
55
43
|
): Promise<T & { deletedAt?: null }> {
|
|
56
|
-
const supportsDeletedAt = await customerPersonCompanyLinksSupportDeletedAt(em)
|
|
57
|
-
if (!supportsDeletedAt) {
|
|
58
|
-
warnMissingCustomerPersonCompanyLinksDeletedAt(source)
|
|
59
|
-
return { ...where }
|
|
60
|
-
}
|
|
61
44
|
return { ...where, deletedAt: null }
|
|
62
45
|
}
|
|
63
46
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Idempotent backstop for `customer_person_company_links.deleted_at`.
|
|
5
|
+
*
|
|
6
|
+
* The column was originally added by `Migration20260415095203` and is
|
|
7
|
+
* referenced by `Migration20260417140000`'s partial unique index
|
|
8
|
+
* (`where "deleted_at" is null`). It is part of the committed
|
|
9
|
+
* `migrations/.snapshot-open-mercato.json`.
|
|
10
|
+
*
|
|
11
|
+
* However, the runtime defensive probe that lived in
|
|
12
|
+
* `packages/core/src/modules/customers/lib/personCompanyLinkTable.ts`
|
|
13
|
+
* historically existed because an aborted-transaction state on a
|
|
14
|
+
* shared connection could (very rarely) make `loadPersonCompanyLinks`
|
|
15
|
+
* fail in a way that *looked* like the column was missing — and the
|
|
16
|
+
* cached fallback then silently dropped the `deletedAt: null` filter.
|
|
17
|
+
* That probe is removed in the same change as this migration (no
|
|
18
|
+
* Kysely query is ever issued from inside an active MikroORM TX);
|
|
19
|
+
* this migration is the matching belt-and-suspenders guarantee that
|
|
20
|
+
* the column is physically present so the unconditional `deletedAt:
|
|
21
|
+
* null` filter cannot 42703.
|
|
22
|
+
*
|
|
23
|
+
* Postgres-native `add column if not exists` is used so this re-applies
|
|
24
|
+
* cleanly on already-migrated databases without raising.
|
|
25
|
+
*/
|
|
26
|
+
export class Migration20260521175027 extends Migration {
|
|
27
|
+
|
|
28
|
+
override async up(): Promise<void> {
|
|
29
|
+
this.addSql(`alter table "customer_person_company_links" add column if not exists "deleted_at" timestamptz null;`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override async down(): Promise<void> {
|
|
33
|
+
// No-op on the way down: dropping the column would break
|
|
34
|
+
// `Migration20260417140000`'s partial unique index, which is the
|
|
35
|
+
// authoritative owner of the column going forward. The original
|
|
36
|
+
// `Migration20260415095203` down still drops it for full rollback.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
@@ -20,85 +20,95 @@ export const openApi = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export async function POST(req: Request) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
try {
|
|
24
|
+
const auth = await getAuthFromRequest(req)
|
|
25
|
+
if (!auth?.tenantId || !auth.orgId) {
|
|
26
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
27
|
+
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const payload = await readJsonSafe(req)
|
|
30
|
+
const parsed = runSyncSchema.safeParse(payload)
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const container = await createRequestContainer()
|
|
36
|
+
const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
|
|
37
|
+
const progressService = container.resolve('progressService') as ProgressService
|
|
38
|
+
const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const scope = {
|
|
41
|
+
organizationId: auth.orgId as string,
|
|
42
|
+
tenantId: auth.tenantId,
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const integration = getIntegration(parsed.data.integrationId)
|
|
46
|
+
if (!integration?.providerKey) {
|
|
47
|
+
return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
|
|
48
|
+
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
const adapter = getDataSyncAdapter(integration.providerKey)
|
|
51
|
+
if (!adapter) {
|
|
52
|
+
return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })
|
|
53
|
+
}
|
|
54
|
+
if (adapter.runMode === 'provider') {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{
|
|
57
|
+
error: 'This integration must be started from its provider-specific import flow.',
|
|
58
|
+
settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,
|
|
59
|
+
},
|
|
60
|
+
{ status: 422 },
|
|
61
|
+
)
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (!adapter.supportedEntities.includes(parsed.data.entityType)) {
|
|
65
|
+
return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })
|
|
66
|
+
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)
|
|
69
|
+
if (!integrationEnabled) {
|
|
70
|
+
return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })
|
|
71
|
+
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
const overlap = await syncRunService.findRunningOverlap(
|
|
74
|
+
parsed.data.integrationId,
|
|
75
|
+
parsed.data.entityType,
|
|
76
|
+
parsed.data.direction,
|
|
77
|
+
scope,
|
|
78
|
+
)
|
|
79
|
+
if (overlap) {
|
|
80
|
+
return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })
|
|
81
|
+
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const cursor = parsed.data.fullSync
|
|
84
|
+
? null
|
|
85
|
+
: await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope)
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
87
|
+
const { run, progressJob } = await startDataSyncRun({
|
|
88
|
+
syncRunService,
|
|
89
|
+
progressService,
|
|
90
|
+
scope: {
|
|
91
|
+
...scope,
|
|
92
|
+
userId: auth.sub,
|
|
93
|
+
},
|
|
94
|
+
input: {
|
|
95
|
+
integrationId: parsed.data.integrationId,
|
|
96
|
+
entityType: parsed.data.entityType,
|
|
97
|
+
direction: parsed.data.direction,
|
|
98
|
+
cursor,
|
|
99
|
+
triggeredBy: parsed.data.triggeredBy ?? auth.sub,
|
|
100
|
+
batchSize: parsed.data.batchSize,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
107
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
108
|
+
console.error('[data_sync.run] unhandled error', { message, stack })
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{ error: 'Failed to start data sync run.', message },
|
|
111
|
+
{ status: 500 },
|
|
112
|
+
)
|
|
113
|
+
}
|
|
104
114
|
}
|
|
@@ -138,27 +138,37 @@ export async function GET(req: Request) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
export async function POST(req: Request) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
try {
|
|
142
|
+
const auth = await getAuthFromRequest(req)
|
|
143
|
+
if (!auth || !auth.tenantId) {
|
|
144
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const body = await req.json()
|
|
148
|
+
const parsed = createProgressJobSchema.safeParse(body)
|
|
149
|
+
if (!parsed.success) {
|
|
150
|
+
return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const container = await createRequestContainer()
|
|
154
|
+
const progressService = container.resolve('progressService') as import('../../lib/progressService').ProgressService
|
|
155
|
+
|
|
156
|
+
const job = await progressService.createJob(parsed.data, {
|
|
157
|
+
tenantId: auth.tenantId,
|
|
158
|
+
organizationId: auth.orgId,
|
|
159
|
+
userId: auth.sub,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return NextResponse.json({ id: job.id }, { status: 201 })
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
165
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
166
|
+
console.error('[progress.jobs.create] unhandled error', { message, stack })
|
|
167
|
+
return NextResponse.json(
|
|
168
|
+
{ error: 'Failed to create progress job.', message },
|
|
169
|
+
{ status: 500 },
|
|
170
|
+
)
|
|
150
171
|
}
|
|
151
|
-
|
|
152
|
-
const container = await createRequestContainer()
|
|
153
|
-
const progressService = container.resolve('progressService') as import('../../lib/progressService').ProgressService
|
|
154
|
-
|
|
155
|
-
const job = await progressService.createJob(parsed.data, {
|
|
156
|
-
tenantId: auth.tenantId,
|
|
157
|
-
organizationId: auth.orgId,
|
|
158
|
-
userId: auth.sub,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
return NextResponse.json({ id: job.id }, { status: 201 })
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
const jobListItemSchema = z.object({
|