@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.
@@ -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 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}\n"],
5
- "mappings": "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",
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.3588.1.2f62d1bb0d",
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.3588.1.2f62d1bb0d",
246
- "@open-mercato/shared": "0.6.3-develop.3588.1.2f62d1bb0d",
247
- "@open-mercato/ui": "0.6.3-develop.3588.1.2f62d1bb0d",
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.3588.1.2f62d1bb0d",
253
- "@open-mercato/shared": "0.6.3-develop.3588.1.2f62d1bb0d",
254
- "@open-mercato/ui": "0.6.3-develop.3588.1.2f62d1bb0d",
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
- await createButton.click();
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
- const PERSON_COMPANY_LINKS_TABLE = 'customer_person_company_links'
5
- const PERSON_COMPANY_LINKS_DELETED_AT_COLUMN = 'deleted_at'
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
- let supportsDeletedAtColumnPromise: Promise<boolean> | null = null
8
- let warnedAboutMissingDeletedAtColumn = false
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(source: string): void {
41
- if (warnedAboutMissingDeletedAtColumn) {
42
- return
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
- em: EntityManager,
40
+ _em: EntityManager,
53
41
  where: T,
54
- source: string,
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
- const auth = await getAuthFromRequest(req)
24
- if (!auth?.tenantId || !auth.orgId) {
25
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
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
- const payload = await readJsonSafe(req)
29
- const parsed = runSyncSchema.safeParse(payload)
30
- if (!parsed.success) {
31
- return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
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
- const container = await createRequestContainer()
35
- const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
36
- const progressService = container.resolve('progressService') as ProgressService
37
- const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService
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
- const scope = {
40
- organizationId: auth.orgId as string,
41
- tenantId: auth.tenantId,
42
- }
40
+ const scope = {
41
+ organizationId: auth.orgId as string,
42
+ tenantId: auth.tenantId,
43
+ }
43
44
 
44
- const integration = getIntegration(parsed.data.integrationId)
45
- if (!integration?.providerKey) {
46
- return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
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
- const adapter = getDataSyncAdapter(integration.providerKey)
50
- if (!adapter) {
51
- return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })
52
- }
53
- if (adapter.runMode === 'provider') {
54
- return NextResponse.json(
55
- {
56
- error: 'This integration must be started from its provider-specific import flow.',
57
- settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,
58
- },
59
- { status: 422 },
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
- if (!adapter.supportedEntities.includes(parsed.data.entityType)) {
64
- return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })
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
- const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)
68
- if (!integrationEnabled) {
69
- return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })
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
- const overlap = await syncRunService.findRunningOverlap(
73
- parsed.data.integrationId,
74
- parsed.data.entityType,
75
- parsed.data.direction,
76
- scope,
77
- )
78
- if (overlap) {
79
- return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })
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
- const cursor = parsed.data.fullSync
83
- ? null
84
- : await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope)
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
- const { run, progressJob } = await startDataSyncRun({
87
- syncRunService,
88
- progressService,
89
- scope: {
90
- ...scope,
91
- userId: auth.sub,
92
- },
93
- input: {
94
- integrationId: parsed.data.integrationId,
95
- entityType: parsed.data.entityType,
96
- direction: parsed.data.direction,
97
- cursor,
98
- triggeredBy: parsed.data.triggeredBy ?? auth.sub,
99
- batchSize: parsed.data.batchSize,
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
- return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })
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
- const auth = await getAuthFromRequest(req)
142
- if (!auth || !auth.tenantId) {
143
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
144
- }
145
-
146
- const body = await req.json()
147
- const parsed = createProgressJobSchema.safeParse(body)
148
- if (!parsed.success) {
149
- return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })
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({