@open-mercato/core 0.6.7-develop.6660.1.90e1e2eef6 → 0.6.7-develop.6669.1.40b669666b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/communication_channels/lib/pg-errors.js +1 -7
  3. package/dist/modules/communication_channels/lib/pg-errors.js.map +2 -2
  4. package/dist/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.js +56 -0
  5. package/dist/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.js.map +7 -0
  6. package/dist/modules/directory/api/organizations/route.js +1 -1
  7. package/dist/modules/directory/api/organizations/route.js.map +1 -1
  8. package/dist/modules/entities/api/definitions.js +2 -0
  9. package/dist/modules/entities/api/definitions.js.map +2 -2
  10. package/dist/modules/entities/lib/helpers.js +1 -0
  11. package/dist/modules/entities/lib/helpers.js.map +2 -2
  12. package/dist/modules/query_index/api/openapi.js +2 -2
  13. package/dist/modules/query_index/api/openapi.js.map +2 -2
  14. package/dist/modules/query_index/api/status.js +6 -4
  15. package/dist/modules/query_index/api/status.js.map +2 -2
  16. package/dist/modules/query_index/cli.js +31 -28
  17. package/dist/modules/query_index/cli.js.map +2 -2
  18. package/dist/modules/query_index/components/QueryIndexesTable.js +4 -1
  19. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  20. package/dist/modules/query_index/lib/batch.js +163 -44
  21. package/dist/modules/query_index/lib/batch.js.map +3 -3
  22. package/dist/modules/query_index/lib/jobs.js +3 -2
  23. package/dist/modules/query_index/lib/jobs.js.map +2 -2
  24. package/dist/modules/query_index/lib/reindexer.js +50 -16
  25. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  26. package/dist/modules/query_index/lib/stale.js +3 -0
  27. package/dist/modules/query_index/lib/stale.js.map +2 -2
  28. package/dist/modules/sales/api/quotes/accept/route.js +7 -2
  29. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  30. package/dist/modules/sales/api/quotes/public/[token]/route.js +2 -1
  31. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  32. package/dist/modules/sales/lib/publicQuoteTenantScope.js +24 -0
  33. package/dist/modules/sales/lib/publicQuoteTenantScope.js.map +7 -0
  34. package/package.json +7 -7
  35. package/src/modules/communication_channels/lib/pg-errors.ts +3 -10
  36. package/src/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.ts +96 -0
  37. package/src/modules/directory/api/organizations/route.ts +1 -1
  38. package/src/modules/entities/api/definitions.ts +2 -0
  39. package/src/modules/entities/lib/helpers.ts +1 -0
  40. package/src/modules/query_index/api/openapi.ts +2 -2
  41. package/src/modules/query_index/api/status.ts +14 -5
  42. package/src/modules/query_index/cli.ts +36 -28
  43. package/src/modules/query_index/components/QueryIndexesTable.tsx +7 -4
  44. package/src/modules/query_index/i18n/de.json +2 -0
  45. package/src/modules/query_index/i18n/en.json +2 -0
  46. package/src/modules/query_index/i18n/es.json +2 -0
  47. package/src/modules/query_index/i18n/pl.json +2 -0
  48. package/src/modules/query_index/lib/batch.ts +244 -67
  49. package/src/modules/query_index/lib/jobs.ts +11 -0
  50. package/src/modules/query_index/lib/reindexer.ts +69 -16
  51. package/src/modules/query_index/lib/stale.ts +10 -0
  52. package/src/modules/sales/api/quotes/accept/route.ts +9 -2
  53. package/src/modules/sales/api/quotes/public/[token]/route.ts +2 -1
  54. package/src/modules/sales/lib/publicQuoteTenantScope.ts +49 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/helpers.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Record<string, CustomFieldDef> | undefined\n if (preferDefs) {\n const defs = await em.find(CustomFieldDef, {\n entityId,\n isActive: true,\n deletedAt: null,\n organizationId: { $in: [organizationId, null] as any },\n tenantId: { $in: [tenantId, null] as any },\n })\n const scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n defsByKey = {}\n for (const d of defs) {\n const existing = defsByKey[d.key]\n if (!existing) {\n defsByKey[d.key] = d\n continue\n }\n const nextScore = scopeScore(d)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n defsByKey[d.key] = d\n continue\n }\n if (nextScore < existingScore) continue\n\n const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime()\n const existingUpdatedAt = existing.updatedAt instanceof Date\n ? existing.updatedAt.getTime()\n : new Date(existing.updatedAt).getTime()\n if (nextUpdatedAt >= existingUpdatedAt) {\n defsByKey[d.key] = d\n }\n }\n }\n\n const toPersist: CustomFieldValue[] = []\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em as any, opts.encryptionService)\n return encryptionService\n }\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.[fieldKey]\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const isArray = Array.isArray(raw)\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = raw as Primitive[]\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)\n : raw\n\n let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { encryptCustomFieldValue, resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport {\n MAX_CUSTOM_FIELD_KEYS_PER_RECORD,\n TOO_MANY_CUSTOM_FIELDS_ERROR,\n} from '@open-mercato/shared/modules/entities/validation'\nimport { CustomFieldDef, CustomFieldValue } from '../data/entities'\n\ntype Primitive = string | number | boolean | null | undefined\ntype PrimitiveOrArray = Primitive | Primitive[]\n\nexport type SetRecordCustomFieldsOptions = {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, PrimitiveOrArray>\n // When true (default), try to use field definitions to decide storage column\n preferDefs?: boolean\n // Optional: notify external systems (e.g., indexing) when values changed\n onChanged?: (payload: { entityId: string; recordId: string; organizationId: string | null; tenantId: string | null }) => Promise<void> | void\n // Optional: re-use an existing tenant encryption service instance\n encryptionService?: TenantDataEncryptionService | null\n}\n\nfunction columnFromKind(kind: string): keyof CustomFieldValue {\n switch (kind) {\n case 'text':\n case 'select':\n case 'currency':\n case 'dictionary':\n case 'phone':\n return 'valueText'\n case 'multiline':\n return 'valueMultiline'\n case 'integer':\n return 'valueInt'\n case 'float':\n return 'valueFloat'\n case 'boolean':\n return 'valueBool'\n default:\n return 'valueText'\n }\n}\n\nfunction columnFromJsValue(v: Primitive): keyof CustomFieldValue {\n if (v === null || v === undefined) return 'valueText'\n if (typeof v === 'boolean') return 'valueBool'\n if (typeof v === 'number') return Number.isInteger(v) ? 'valueInt' : 'valueFloat'\n return 'valueText'\n}\n\n// Clears all value columns to avoid leftovers on update\nfunction clearValueColumns(cf: CustomFieldValue) {\n cf.valueText = null\n cf.valueMultiline = null\n cf.valueInt = null\n cf.valueFloat = null\n cf.valueBool = null\n}\n\nexport async function setRecordCustomFields(\n em: EntityManager,\n opts: SetRecordCustomFieldsOptions,\n): Promise<void> {\n const { entityId, recordId, values } = opts\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const preferDefs = opts.preferDefs !== false\n\n let defsByKey: Record<string, CustomFieldDef> | undefined\n if (preferDefs) {\n const defs = await em.find(CustomFieldDef, {\n entityId,\n isActive: true,\n deletedAt: null,\n organizationId: { $in: [organizationId, null] as any },\n tenantId: { $in: [tenantId, null] as any },\n })\n const scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n defsByKey = {}\n for (const d of defs) {\n const existing = defsByKey[d.key]\n if (!existing) {\n defsByKey[d.key] = d\n continue\n }\n const nextScore = scopeScore(d)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n defsByKey[d.key] = d\n continue\n }\n if (nextScore < existingScore) continue\n\n const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime()\n const existingUpdatedAt = existing.updatedAt instanceof Date\n ? existing.updatedAt.getTime()\n : new Date(existing.updatedAt).getTime()\n if (nextUpdatedAt >= existingUpdatedAt) {\n defsByKey[d.key] = d\n }\n }\n }\n\n const toPersist: CustomFieldValue[] = []\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em as any, opts.encryptionService)\n return encryptionService\n }\n const keys = Object.keys(values)\n const presentKeyCount = keys.filter((key) => values[key] !== undefined).length\n if (preferDefs && presentKeyCount > MAX_CUSTOM_FIELD_KEYS_PER_RECORD) {\n throw new Error(TOO_MANY_CUSTOM_FIELDS_ERROR)\n }\n\n // Run the per-key delete+insert work inside ONE database transaction so a\n // multi-value replacement is atomic and isolated. The array branch deletes the\n // existing rows for a key and inserts the replacements; without an enclosing\n // transaction those can land in separate commit boundaries under MikroORM's\n // FlushMode.AUTO (a query elsewhere in the unit auto-flushes part of the work),\n // which intermittently left the field with the delete applied but the inserts\n // missing \u2014 the multi-select EDIT reverted to []. The single commit below makes\n // it all-or-nothing. We only open our own transaction when the caller has not\n // already started one (commands fork the request em and may run setCustomFields\n // outside their own withAtomicFlush tx); join an ambient transaction otherwise.\n const txEm = em as {\n begin?: () => Promise<void>\n commit?: () => Promise<void>\n rollback?: () => Promise<void>\n isInTransaction?: () => boolean\n }\n const txCapable =\n typeof txEm.begin === 'function' &&\n typeof txEm.commit === 'function' &&\n typeof txEm.rollback === 'function' &&\n typeof txEm.isInTransaction === 'function'\n const ownCustomFieldTransaction = txCapable && !txEm.isInTransaction!()\n if (ownCustomFieldTransaction) await txEm.begin!()\n try {\n for (const fieldKey of keys) {\n const raw = values[fieldKey]\n if (raw === undefined) continue\n\n const def = defsByKey?.[fieldKey]\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const isArray = Array.isArray(raw)\n // When array (multi-value): replace all existing rows for the key. Delete\n // first, then create replacements, all inside the transaction opened above.\n // Creating rows before a native delete can auto-flush and delete the new\n // values; mixing em.remove(stale) with new rows for the same EAV scope was\n // observed to commit an empty set under MikroORM v7. The explicit order keeps\n // the replacement atomic without letting old-row cleanup target new rows.\n if (isArray) {\n const arr = raw as Primitive[]\n await em.nativeDelete(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n for (const val of arr) {\n const col: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(val)\n const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n clearValueColumns(cf)\n const stored = encrypted\n ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)\n : val\n switch (col) {\n case 'valueText': cf.valueText = stored == null ? null : String(stored); break\n case 'valueMultiline': cf.valueMultiline = stored == null ? null : String(stored); break\n case 'valueInt': cf.valueInt = stored == null ? null : Number(stored); break\n case 'valueFloat': cf.valueFloat = stored == null ? null : Number(stored); break\n case 'valueBool': cf.valueBool = stored == null ? null : Boolean(stored); break\n default: cf.valueText = stored == null ? null : String(stored); break\n }\n toPersist.push(cf)\n }\n continue\n }\n\n const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)\n const storedValue = encrypted\n ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)\n : raw\n\n let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })\n if (!cf) {\n cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })\n toPersist.push(cf)\n }\n clearValueColumns(cf)\n switch (column) {\n case 'valueText':\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueMultiline':\n cf.valueMultiline = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n case 'valueInt':\n cf.valueInt = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueFloat':\n cf.valueFloat = (storedValue as Primitive) == null ? null : Number(storedValue as Primitive)\n break\n case 'valueBool':\n cf.valueBool = (storedValue as Primitive) == null ? null : Boolean(storedValue as Primitive)\n break\n default:\n cf.valueText = (storedValue as Primitive) == null ? null : String(storedValue as Primitive)\n break\n }\n }\n\n if (toPersist.length) em.persist(toPersist)\n await em.flush()\n if (ownCustomFieldTransaction) await txEm.commit!()\n } catch (err) {\n if (ownCustomFieldTransaction) {\n try { await txEm.rollback!() } catch { /* surface the original error, not a rollback failure */ }\n }\n throw err\n }\n // Emit hook for indexing if requested (outside CRUD flows). Runs AFTER the\n // transaction commits so consumers observe the persisted rows.\n try {\n if (typeof opts.onChanged === 'function') {\n await opts.onChanged({ entityId, recordId, organizationId, tenantId })\n }\n } catch {\n // Non-blocking\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["cf"]
7
7
  }
@@ -7,7 +7,7 @@ const queryIndexErrorSchema = z.object({
7
7
  const queryIndexPartitionSchema = z.object({
8
8
  partitionIndex: z.number().int().nonnegative().nullable().optional(),
9
9
  partitionCount: z.number().int().positive().nullable().optional(),
10
- status: z.enum(["reindexing", "purging", "stalled", "completed"]),
10
+ status: z.enum(["reindexing", "purging", "stalled", "completed", "failed"]),
11
11
  startedAt: z.string().nullable().optional(),
12
12
  finishedAt: z.string().nullable().optional(),
13
13
  heartbeatAt: z.string().nullable().optional(),
@@ -15,7 +15,7 @@ const queryIndexPartitionSchema = z.object({
15
15
  totalCount: z.number().int().nonnegative().nullable().optional()
16
16
  });
17
17
  const queryIndexJobSchema = z.object({
18
- status: z.enum(["idle", "reindexing", "purging", "stalled"]),
18
+ status: z.enum(["idle", "reindexing", "purging", "stalled", "failed"]),
19
19
  startedAt: z.string().nullable().optional(),
20
20
  finishedAt: z.string().nullable().optional(),
21
21
  heartbeatAt: z.string().nullable().optional(),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/api/openapi.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { ENTITY_ID_PATTERN } from '@open-mercato/shared/lib/query/engine'\n\nexport const queryIndexTag = 'Query Index'\n\nexport const queryIndexErrorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const queryIndexPartitionSchema = z.object({\n partitionIndex: z.number().int().nonnegative().nullable().optional(),\n partitionCount: z.number().int().positive().nullable().optional(),\n status: z.enum(['reindexing', 'purging', 'stalled', 'completed']),\n startedAt: z.string().nullable().optional(),\n finishedAt: z.string().nullable().optional(),\n heartbeatAt: z.string().nullable().optional(),\n processedCount: z.number().int().nonnegative().nullable().optional(),\n totalCount: z.number().int().nonnegative().nullable().optional(),\n})\n\nexport const queryIndexJobSchema = z.object({\n status: z.enum(['idle', 'reindexing', 'purging', 'stalled']),\n startedAt: z.string().nullable().optional(),\n finishedAt: z.string().nullable().optional(),\n heartbeatAt: z.string().nullable().optional(),\n processedCount: z.number().int().nonnegative().nullable().optional(),\n totalCount: z.number().int().nonnegative().nullable().optional(),\n partitions: z.array(queryIndexPartitionSchema).optional(),\n scope: queryIndexPartitionSchema.pick({\n status: true,\n processedCount: true,\n totalCount: true,\n })\n .nullable()\n .optional(),\n})\n\nexport const queryIndexStatusItemSchema = z.object({\n entityId: z.string(),\n label: z.string(),\n baseCount: z.number().int().nonnegative().nullable(),\n indexCount: z.number().int().nonnegative().nullable(),\n vectorCount: z.number().int().nonnegative().nullable().optional(),\n vectorEnabled: z.boolean().optional(),\n ok: z.boolean(),\n job: queryIndexJobSchema,\n})\n\nexport const queryIndexErrorLogSchema = z.object({\n id: z.string(),\n source: z.string(),\n handler: z.string(),\n entityType: z.string().nullable(),\n recordId: z.string().nullable(),\n tenantId: z.string().nullable(),\n organizationId: z.string().nullable(),\n message: z.string(),\n stack: z.string().nullable(),\n payload: z.unknown().nullable(),\n occurredAt: z.string(),\n})\n\nexport const queryIndexStatusLogSchema = z.object({\n id: z.string(),\n source: z.string(),\n handler: z.string(),\n level: z.enum(['info', 'warn']),\n entityType: z.string().nullable(),\n recordId: z.string().nullable(),\n tenantId: z.string().nullable(),\n organizationId: z.string().nullable(),\n message: z.string(),\n details: z.unknown().nullable(),\n occurredAt: z.string(),\n})\n\nexport const queryIndexStatusResponseSchema = z.object({\n items: z.array(queryIndexStatusItemSchema),\n errors: z.array(queryIndexErrorLogSchema),\n logs: z.array(queryIndexStatusLogSchema),\n})\n\nexport const queryIndexReindexRequestSchema = z.object({\n entityType: z.string().min(1).regex(ENTITY_ID_PATTERN),\n force: z.boolean().optional(),\n batchSize: z.number().int().positive().optional(),\n partitionCount: z.number().int().positive().optional(),\n partitionIndex: z.number().int().nonnegative().optional(),\n})\n\nexport const queryIndexPurgeRequestSchema = z.object({\n entityType: z.string().min(1),\n})\n\nexport const queryIndexOkSchema = z.object({\n ok: z.literal(true),\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { ENTITY_ID_PATTERN } from '@open-mercato/shared/lib/query/engine'\n\nexport const queryIndexTag = 'Query Index'\n\nexport const queryIndexErrorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const queryIndexPartitionSchema = z.object({\n partitionIndex: z.number().int().nonnegative().nullable().optional(),\n partitionCount: z.number().int().positive().nullable().optional(),\n status: z.enum(['reindexing', 'purging', 'stalled', 'completed', 'failed']),\n startedAt: z.string().nullable().optional(),\n finishedAt: z.string().nullable().optional(),\n heartbeatAt: z.string().nullable().optional(),\n processedCount: z.number().int().nonnegative().nullable().optional(),\n totalCount: z.number().int().nonnegative().nullable().optional(),\n})\n\nexport const queryIndexJobSchema = z.object({\n status: z.enum(['idle', 'reindexing', 'purging', 'stalled', 'failed']),\n startedAt: z.string().nullable().optional(),\n finishedAt: z.string().nullable().optional(),\n heartbeatAt: z.string().nullable().optional(),\n processedCount: z.number().int().nonnegative().nullable().optional(),\n totalCount: z.number().int().nonnegative().nullable().optional(),\n partitions: z.array(queryIndexPartitionSchema).optional(),\n scope: queryIndexPartitionSchema.pick({\n status: true,\n processedCount: true,\n totalCount: true,\n })\n .nullable()\n .optional(),\n})\n\nexport const queryIndexStatusItemSchema = z.object({\n entityId: z.string(),\n label: z.string(),\n baseCount: z.number().int().nonnegative().nullable(),\n indexCount: z.number().int().nonnegative().nullable(),\n vectorCount: z.number().int().nonnegative().nullable().optional(),\n vectorEnabled: z.boolean().optional(),\n ok: z.boolean(),\n job: queryIndexJobSchema,\n})\n\nexport const queryIndexErrorLogSchema = z.object({\n id: z.string(),\n source: z.string(),\n handler: z.string(),\n entityType: z.string().nullable(),\n recordId: z.string().nullable(),\n tenantId: z.string().nullable(),\n organizationId: z.string().nullable(),\n message: z.string(),\n stack: z.string().nullable(),\n payload: z.unknown().nullable(),\n occurredAt: z.string(),\n})\n\nexport const queryIndexStatusLogSchema = z.object({\n id: z.string(),\n source: z.string(),\n handler: z.string(),\n level: z.enum(['info', 'warn']),\n entityType: z.string().nullable(),\n recordId: z.string().nullable(),\n tenantId: z.string().nullable(),\n organizationId: z.string().nullable(),\n message: z.string(),\n details: z.unknown().nullable(),\n occurredAt: z.string(),\n})\n\nexport const queryIndexStatusResponseSchema = z.object({\n items: z.array(queryIndexStatusItemSchema),\n errors: z.array(queryIndexErrorLogSchema),\n logs: z.array(queryIndexStatusLogSchema),\n})\n\nexport const queryIndexReindexRequestSchema = z.object({\n entityType: z.string().min(1).regex(ENTITY_ID_PATTERN),\n force: z.boolean().optional(),\n batchSize: z.number().int().positive().optional(),\n partitionCount: z.number().int().positive().optional(),\n partitionIndex: z.number().int().nonnegative().optional(),\n})\n\nexport const queryIndexPurgeRequestSchema = z.object({\n entityType: z.string().min(1),\n})\n\nexport const queryIndexOkSchema = z.object({\n ok: z.literal(true),\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -170,7 +170,7 @@ async function GET(req) {
170
170
  const startedDate = row.started_at ? new Date(row.started_at) : null;
171
171
  const finishedDate = row.finished_at ? new Date(row.finished_at) : null;
172
172
  const stalled = !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS);
173
- const state = finishedDate ? "completed" : stalled ? "stalled" : row.status || "reindexing";
173
+ const state = finishedDate ? row.status === "failed" ? "failed" : "completed" : stalled ? "stalled" : row.status || "reindexing";
174
174
  return {
175
175
  partitionIndex: row.partition_index ?? null,
176
176
  partitionCount: row.partition_count ?? null,
@@ -187,6 +187,7 @@ async function GET(req) {
187
187
  (p) => p.status === "reindexing" || p.status === "purging"
188
188
  );
189
189
  const stalledPartitions = activePartitions.filter((p) => p.status === "stalled");
190
+ const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null;
190
191
  let status = "idle";
191
192
  if (activePartitions.length) {
192
193
  if (runningPartitions.length) {
@@ -194,14 +195,15 @@ async function GET(req) {
194
195
  } else if (stalledPartitions.length) {
195
196
  status = "stalled";
196
197
  }
198
+ } else if (partitions.some((p) => p.status === "failed") || scopeCandidate?.row.finished_at && scopeCandidate.row.status === "failed") {
199
+ status = "failed";
197
200
  }
198
201
  const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null;
199
- const finishedAt = status === "idle" ? partitions.find((p) => p.finishedAt)?.finishedAt ?? null : null;
202
+ const finishedAt = status === "idle" || status === "failed" ? partitions.find((p) => p.finishedAt)?.finishedAt ?? null : null;
200
203
  const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null;
201
204
  const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0);
202
205
  const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0);
203
206
  const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null;
204
- const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null;
205
207
  return {
206
208
  status,
207
209
  startedAt,
@@ -214,7 +216,7 @@ async function GET(req) {
214
216
  status: (() => {
215
217
  const heartbeatDate = scopeCandidate.row.heartbeat_at ? new Date(scopeCandidate.row.heartbeat_at) : null;
216
218
  const finishedDate = scopeCandidate.row.finished_at ? new Date(scopeCandidate.row.finished_at) : null;
217
- if (finishedDate) return "completed";
219
+ if (finishedDate) return scopeCandidate.row.status === "failed" ? "failed" : "completed";
218
220
  if (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS) {
219
221
  return "stalled";
220
222
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/api/status.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { readCoverageSnapshots, refreshCoverageSnapshot, type CoverageSnapshot } from '../lib/coverage'\nimport { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from './openapi'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['query_index.status.view'] },\n}\n\nconst STATUS_REFRESH_COOLDOWN_MS = 60_000\n\nfunction getCoverageSnapshotRefreshedAt(snapshot: Pick<CoverageSnapshot, 'refreshed_at'> | null | undefined): number | null {\n const value = snapshot?.refreshed_at\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isFinite(time) ? time : null\n }\n if (typeof value === 'string') {\n const time = new Date(value).getTime()\n return Number.isFinite(time) ? time : null\n }\n return null\n}\n\nfunction hasFreshCoverageSnapshots(\n snapshots: Map<string, CoverageSnapshot>,\n entityIds: string[],\n now: number,\n): boolean {\n for (const entityId of entityIds) {\n const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId))\n if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false\n }\n return entityIds.length > 0\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const db = (em as any).getKysely()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = typeof scope.tenantId === 'string' && scope.tenantId.trim().length > 0\n ? scope.tenantId.trim()\n : (typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null)\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const organizationFilter =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : organizationId\n ? [organizationId]\n : []\n\n if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const organizationScopeIds = organizationFilter === null\n ? null\n : Array.from(\n new Set(\n organizationFilter.filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n ),\n ),\n )\n\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const forceRefresh = url.searchParams.has('refresh') && url.searchParams.get('refresh') !== '0'\n\n const generatedIds = flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }))\n\n const byId = new Map<string, { entityId: string; label: string }>()\n for (const g of generated) byId.set(g.entityId, g)\n\n let entityIds = generatedIds.slice()\n\n // Resolve search module configs to determine vector-enabled entities\n // Entities with buildSource defined are vector-search enabled\n let searchModuleConfigs: SearchModuleConfig[] = []\n try {\n searchModuleConfigs = container.resolve('searchModuleConfigs') as SearchModuleConfig[]\n } catch {\n // Search module configs not available\n }\n\n const vectorEnabledEntities = new Set<string>()\n const fulltextEnabledEntities = new Set<string>()\n for (const moduleConfig of searchModuleConfigs) {\n for (const entity of moduleConfig.entities ?? []) {\n if (entity.enabled !== false) {\n // Vector: entities with buildSource defined\n if (typeof entity.buildSource === 'function') {\n vectorEnabledEntities.add(entity.entityId)\n }\n // Fulltext: entities with fieldPolicy defined\n if (entity.fieldPolicy && typeof entity.fieldPolicy === 'object') {\n fulltextEnabledEntities.add(entity.entityId)\n }\n }\n }\n }\n\n // Resolve fulltext strategy for entity counts\n let fulltextStrategy: FullTextSearchStrategy | null = null\n try {\n const searchStrategies = container.resolve('searchStrategies') as unknown[]\n fulltextStrategy = (searchStrategies?.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy) ?? null\n } catch {\n fulltextStrategy = null\n }\n\n // Fetch fulltext entity counts\n let fulltextEntityCounts: Record<string, number> | null = null\n if (fulltextStrategy) {\n try {\n fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId)\n } catch {\n fulltextEntityCounts = null\n }\n }\n\n // Limit to entities that have active custom field definitions in current scope\n try {\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .where('is_active' as any, '=', true)\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds)) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, 'in', organizationScopeIds),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n const cfRows = await cfQuery.execute() as Array<{ entity_id: string }>\n const enabled = new Set<string>((cfRows || []).map((r) => String(r.entity_id)))\n entityIds = entityIds.filter((id) => enabled.has(id))\n } catch {}\n\n const HEARTBEAT_STALE_MS = 60_000\n const COVERAGE_STALE_MS = 60_000\n const COVERAGE_REFRESH_CONCURRENCY = 8\n\n async function fetchJobSummary(entityType: string, tenantIdParam: string | null, organizationIdParam: string | null) {\n try {\n let jobQuery = db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, '=', entityType)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantIdParam ?? null}`)\n if (organizationIdParam != null) {\n jobQuery = jobQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationIdParam),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n jobQuery = jobQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)\n }\n const rows = await jobQuery\n .orderBy('started_at' as any, 'desc')\n .execute() as Array<Record<string, any>>\n\n if (!rows.length) {\n return { status: 'idle' as const, partitions: [] as any[] }\n }\n\n const preferOrg =\n organizationIdParam != null && rows.some((row: any) => row.organization_id === organizationIdParam)\n const pickPreferred = <T extends { startedTs: number; tenantMatch: boolean; orgMatch: boolean }>(\n existing: T | null,\n candidate: T,\n ): T => {\n if (!existing) return candidate\n if (preferOrg) {\n if (candidate.orgMatch && !existing.orgMatch) return candidate\n if (!candidate.orgMatch && existing.orgMatch) return existing\n }\n if (candidate.tenantMatch && !existing.tenantMatch) return candidate\n if (!candidate.tenantMatch && existing.tenantMatch) return existing\n return candidate.startedTs > existing.startedTs ? candidate : existing\n }\n\n const partitionRows = new Map<string, { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean }>()\n let scopeRow: { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean } | null = null\n for (const row of rows) {\n const key = String(row.partition_index ?? '__null__')\n const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0\n const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true\n const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null\n const candidate = { row, startedTs, tenantMatch, orgMatch }\n if (row.partition_index == null) {\n scopeRow = pickPreferred(scopeRow, candidate)\n continue\n }\n const existing = partitionRows.get(key)\n partitionRows.set(key, pickPreferred(existing ?? null, candidate))\n }\n\n const partitions = Array.from(partitionRows.values())\n .filter((entry) => !preferOrg || entry.orgMatch)\n .map(({ row }) => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const startedDate = row.started_at ? new Date(row.started_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n const stalled =\n !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)\n const state = finishedDate\n ? 'completed'\n : stalled\n ? 'stalled'\n : (row.status as string) || 'reindexing'\n return {\n partitionIndex: row.partition_index ?? null,\n partitionCount: row.partition_count ?? null,\n status: state,\n startedAt: startedDate ? startedDate.toISOString() : null,\n finishedAt: finishedDate ? finishedDate.toISOString() : null,\n heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,\n processedCount: row.processed_count ?? null,\n totalCount: row.total_count ?? null,\n }\n })\n .sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0))\n const activePartitions = partitions.filter((p) => !p.finishedAt)\n const runningPartitions = activePartitions.filter(\n (p) => p.status === 'reindexing' || p.status === 'purging',\n )\n const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')\n let status: 'idle' | 'reindexing' | 'purging' | 'stalled' = 'idle'\n if (activePartitions.length) {\n if (runningPartitions.length) {\n status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'\n } else if (stalledPartitions.length) {\n status = 'stalled'\n }\n }\n\n const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null\n const finishedAt = status === 'idle' ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null) : null\n const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null\n const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)\n const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)\n const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null\n const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null\n\n return {\n status,\n startedAt,\n finishedAt,\n heartbeatAt,\n processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,\n totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,\n partitions,\n scope: scopeCandidate\n ? {\n status: (() => {\n const heartbeatDate = scopeCandidate!.row.heartbeat_at ? new Date(scopeCandidate!.row.heartbeat_at) : null\n const finishedDate = scopeCandidate!.row.finished_at ? new Date(scopeCandidate!.row.finished_at) : null\n if (finishedDate) return 'completed'\n if (\n !heartbeatDate ||\n Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS\n ) {\n return 'stalled'\n }\n return (scopeCandidate!.row.status as string) || 'reindexing'\n })(),\n processedCount: scopeCandidate.row.processed_count ?? null,\n totalCount: scopeCandidate.row.total_count ?? null,\n }\n : null,\n }\n } catch {\n return { status: 'idle' as const, partitions: [] as any[] }\n }\n }\n\n const normalizeCount = (value: unknown): number | null => {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n\n const coverageScope = {\n tenantId: tenantId ?? null,\n organizationId,\n withDeleted: false,\n } as const\n const entitiesNeedingRefresh = new Set<string>()\n\n // Read every entity's coverage snapshot in a single batched query. This endpoint is\n // polled by the status table every few seconds, so the poll path must stay read-cheap:\n // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh\n // event emitted below, never inline per entity.\n const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n\n // An explicit refresh action (?refresh) may block, but only when the durable\n // coverage snapshots are stale. Recent persisted snapshots survive workers/restarts,\n // so repeated refresh requests use them instead of hammering base-table counts.\n if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {\n await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>\n refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),\n )\n const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)\n }\n\n const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)\n\n const jobs = await Promise.all(entityIds.map((eid) => fetchJobSummary(eid, tenantId, organizationId)))\n\n const items: any[] = []\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const eid = entityIds[idx]\n let coverage = coverageSnapshots[idx]\n\n const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null\n const isStale = !coverage || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)\n if (isStale) entitiesNeedingRefresh.add(eid)\n\n const job = jobs[idx]\n const label = (byId.get(eid)?.label) || eid\n const baseCountNumber = normalizeCount(coverage?.baseCount)\n const indexCountNumber = normalizeCount(coverage?.indexedCount)\n const vectorEnabled = vectorEnabledEntities.has(eid)\n const vectorCountNumber = vectorEnabled ? normalizeCount((coverage as any)?.vectorIndexedCount ?? (coverage as any)?.vector_indexed_count) : null\n const fulltextEnabled = fulltextEnabledEntities.has(eid)\n const fulltextCountNumber = fulltextEnabled ? (fulltextEntityCounts?.[eid] ?? 0) : null\n const ok = (() => {\n if (baseCountNumber == null || indexCountNumber == null) return false\n if (baseCountNumber !== indexCountNumber) return false\n if (!vectorEnabled) return true\n return vectorCountNumber != null && vectorCountNumber === baseCountNumber\n })()\n items.push({\n entityId: eid,\n label,\n baseCount: baseCountNumber,\n indexCount: indexCountNumber,\n vectorCount: vectorEnabled ? vectorCountNumber : null,\n vectorEnabled,\n fulltextCount: fulltextCountNumber,\n fulltextEnabled,\n ok,\n job,\n refreshedAt: refreshedAt ?? null,\n })\n }\n\n if (!forceRefresh) {\n try {\n const eventBus = container.resolve('eventBus')\n if (entitiesNeedingRefresh.size > 0) {\n await Promise.all(\n Array.from(entitiesNeedingRefresh).map((entityId) =>\n eventBus\n .emitEvent('query_index.coverage.refresh', {\n entityType: entityId,\n tenantId: tenantId ?? null,\n organizationId,\n delayMs: 0,\n })\n .catch(() => undefined)\n )\n )\n }\n } catch {}\n }\n\n let errorQuery = db\n .selectFrom('indexer_error_logs' as any)\n .selectAll()\n if (tenantId != null) {\n errorQuery = errorQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n errorQuery = errorQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n errorQuery = errorQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n errorQuery = errorQuery.where('organization_id' as any, 'is', null as any)\n }\n const errorRows = await errorQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const errors = errorRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n stack: row.stack ?? null,\n payload: row.payload ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n let logsQuery = db\n .selectFrom('indexer_status_logs' as any)\n .selectAll()\n if (tenantId != null) {\n logsQuery = logsQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n logsQuery = logsQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n logsQuery = logsQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n logsQuery = logsQuery.where('organization_id' as any, 'is', null as any)\n }\n const logRows = await logsQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const logs = logRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n const level = row.level === 'warn' ? 'warn' : 'info'\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n level,\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n details: row.details ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n const response = NextResponse.json({ items, errors, logs })\n const partial = items.find((item) => {\n // Coverage not computed yet (no snapshot) \u2014 pending an async refresh, not a partial\n // index. Do not raise the partial-index warning while counts are still unknown.\n if (item.baseCount == null && item.indexCount == null) return false\n if (item.baseCount == null || item.indexCount == null) return true\n return item.baseCount !== item.indexCount\n })\n if (partial) {\n response.headers.set(\n 'x-om-partial-index',\n JSON.stringify({\n type: 'partial_index',\n entity: partial.entityId,\n entityLabel: partial.label ?? partial.entityId,\n baseCount: partial.baseCount,\n indexedCount: partial.indexCount,\n scope: organizationId,\n })\n )\n }\n return response\n}\n\nconst queryIndexStatusDoc: OpenApiMethodDoc = {\n summary: 'Inspect query index coverage',\n description: 'Returns entity counts comparing base tables with the query index along with the latest job status.',\n tags: [queryIndexTag],\n responses: [\n { status: 200, description: 'Current query index status.', schema: queryIndexStatusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Tenant or organization context required', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Query index status',\n methods: {\n GET: queryIndexStatusDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { readCoverageSnapshots, refreshCoverageSnapshot, type CoverageSnapshot } from '../lib/coverage'\nimport { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from './openapi'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['query_index.status.view'] },\n}\n\nconst STATUS_REFRESH_COOLDOWN_MS = 60_000\n\nfunction getCoverageSnapshotRefreshedAt(snapshot: Pick<CoverageSnapshot, 'refreshed_at'> | null | undefined): number | null {\n const value = snapshot?.refreshed_at\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isFinite(time) ? time : null\n }\n if (typeof value === 'string') {\n const time = new Date(value).getTime()\n return Number.isFinite(time) ? time : null\n }\n return null\n}\n\nfunction hasFreshCoverageSnapshots(\n snapshots: Map<string, CoverageSnapshot>,\n entityIds: string[],\n now: number,\n): boolean {\n for (const entityId of entityIds) {\n const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId))\n if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false\n }\n return entityIds.length > 0\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const db = (em as any).getKysely()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = typeof scope.tenantId === 'string' && scope.tenantId.trim().length > 0\n ? scope.tenantId.trim()\n : (typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null)\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const organizationFilter =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : organizationId\n ? [organizationId]\n : []\n\n if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const organizationScopeIds = organizationFilter === null\n ? null\n : Array.from(\n new Set(\n organizationFilter.filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n ),\n ),\n )\n\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const forceRefresh = url.searchParams.has('refresh') && url.searchParams.get('refresh') !== '0'\n\n const generatedIds = flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }))\n\n const byId = new Map<string, { entityId: string; label: string }>()\n for (const g of generated) byId.set(g.entityId, g)\n\n let entityIds = generatedIds.slice()\n\n // Resolve search module configs to determine vector-enabled entities\n // Entities with buildSource defined are vector-search enabled\n let searchModuleConfigs: SearchModuleConfig[] = []\n try {\n searchModuleConfigs = container.resolve('searchModuleConfigs') as SearchModuleConfig[]\n } catch {\n // Search module configs not available\n }\n\n const vectorEnabledEntities = new Set<string>()\n const fulltextEnabledEntities = new Set<string>()\n for (const moduleConfig of searchModuleConfigs) {\n for (const entity of moduleConfig.entities ?? []) {\n if (entity.enabled !== false) {\n // Vector: entities with buildSource defined\n if (typeof entity.buildSource === 'function') {\n vectorEnabledEntities.add(entity.entityId)\n }\n // Fulltext: entities with fieldPolicy defined\n if (entity.fieldPolicy && typeof entity.fieldPolicy === 'object') {\n fulltextEnabledEntities.add(entity.entityId)\n }\n }\n }\n }\n\n // Resolve fulltext strategy for entity counts\n let fulltextStrategy: FullTextSearchStrategy | null = null\n try {\n const searchStrategies = container.resolve('searchStrategies') as unknown[]\n fulltextStrategy = (searchStrategies?.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy) ?? null\n } catch {\n fulltextStrategy = null\n }\n\n // Fetch fulltext entity counts\n let fulltextEntityCounts: Record<string, number> | null = null\n if (fulltextStrategy) {\n try {\n fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId)\n } catch {\n fulltextEntityCounts = null\n }\n }\n\n // Limit to entities that have active custom field definitions in current scope\n try {\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .where('is_active' as any, '=', true)\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds)) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, 'in', organizationScopeIds),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n const cfRows = await cfQuery.execute() as Array<{ entity_id: string }>\n const enabled = new Set<string>((cfRows || []).map((r) => String(r.entity_id)))\n entityIds = entityIds.filter((id) => enabled.has(id))\n } catch {}\n\n const HEARTBEAT_STALE_MS = 60_000\n const COVERAGE_STALE_MS = 60_000\n const COVERAGE_REFRESH_CONCURRENCY = 8\n\n async function fetchJobSummary(entityType: string, tenantIdParam: string | null, organizationIdParam: string | null) {\n try {\n let jobQuery = db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, '=', entityType)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantIdParam ?? null}`)\n if (organizationIdParam != null) {\n jobQuery = jobQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationIdParam),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n jobQuery = jobQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)\n }\n const rows = await jobQuery\n .orderBy('started_at' as any, 'desc')\n .execute() as Array<Record<string, any>>\n\n if (!rows.length) {\n return { status: 'idle' as const, partitions: [] as any[] }\n }\n\n const preferOrg =\n organizationIdParam != null && rows.some((row: any) => row.organization_id === organizationIdParam)\n const pickPreferred = <T extends { startedTs: number; tenantMatch: boolean; orgMatch: boolean }>(\n existing: T | null,\n candidate: T,\n ): T => {\n if (!existing) return candidate\n if (preferOrg) {\n if (candidate.orgMatch && !existing.orgMatch) return candidate\n if (!candidate.orgMatch && existing.orgMatch) return existing\n }\n if (candidate.tenantMatch && !existing.tenantMatch) return candidate\n if (!candidate.tenantMatch && existing.tenantMatch) return existing\n return candidate.startedTs > existing.startedTs ? candidate : existing\n }\n\n const partitionRows = new Map<string, { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean }>()\n let scopeRow: { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean } | null = null\n for (const row of rows) {\n const key = String(row.partition_index ?? '__null__')\n const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0\n const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true\n const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null\n const candidate = { row, startedTs, tenantMatch, orgMatch }\n if (row.partition_index == null) {\n scopeRow = pickPreferred(scopeRow, candidate)\n continue\n }\n const existing = partitionRows.get(key)\n partitionRows.set(key, pickPreferred(existing ?? null, candidate))\n }\n\n const partitions = Array.from(partitionRows.values())\n .filter((entry) => !preferOrg || entry.orgMatch)\n .map(({ row }) => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const startedDate = row.started_at ? new Date(row.started_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n const stalled =\n !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)\n const state = finishedDate\n ? (row.status === 'failed' ? 'failed' : 'completed')\n : stalled\n ? 'stalled'\n : (row.status as string) || 'reindexing'\n return {\n partitionIndex: row.partition_index ?? null,\n partitionCount: row.partition_count ?? null,\n status: state,\n startedAt: startedDate ? startedDate.toISOString() : null,\n finishedAt: finishedDate ? finishedDate.toISOString() : null,\n heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,\n processedCount: row.processed_count ?? null,\n totalCount: row.total_count ?? null,\n }\n })\n .sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0))\n const activePartitions = partitions.filter((p) => !p.finishedAt)\n const runningPartitions = activePartitions.filter(\n (p) => p.status === 'reindexing' || p.status === 'purging',\n )\n const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')\n const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null\n let status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed' = 'idle'\n if (activePartitions.length) {\n if (runningPartitions.length) {\n status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'\n } else if (stalledPartitions.length) {\n status = 'stalled'\n }\n } else if (\n partitions.some((p) => p.status === 'failed')\n || (scopeCandidate?.row.finished_at && scopeCandidate.row.status === 'failed')\n ) {\n // The run finished but lost records; without this it reports \"idle\" and the only\n // hint that anything went wrong is the coverage percentage.\n status = 'failed'\n }\n\n const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null\n const finishedAt = status === 'idle' || status === 'failed'\n ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null)\n : null\n const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null\n const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)\n const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)\n const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null\n\n return {\n status,\n startedAt,\n finishedAt,\n heartbeatAt,\n processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,\n totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,\n partitions,\n scope: scopeCandidate\n ? {\n status: (() => {\n const heartbeatDate = scopeCandidate!.row.heartbeat_at ? new Date(scopeCandidate!.row.heartbeat_at) : null\n const finishedDate = scopeCandidate!.row.finished_at ? new Date(scopeCandidate!.row.finished_at) : null\n if (finishedDate) return scopeCandidate!.row.status === 'failed' ? 'failed' : 'completed'\n if (\n !heartbeatDate ||\n Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS\n ) {\n return 'stalled'\n }\n return (scopeCandidate!.row.status as string) || 'reindexing'\n })(),\n processedCount: scopeCandidate.row.processed_count ?? null,\n totalCount: scopeCandidate.row.total_count ?? null,\n }\n : null,\n }\n } catch {\n return { status: 'idle' as const, partitions: [] as any[] }\n }\n }\n\n const normalizeCount = (value: unknown): number | null => {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n\n const coverageScope = {\n tenantId: tenantId ?? null,\n organizationId,\n withDeleted: false,\n } as const\n const entitiesNeedingRefresh = new Set<string>()\n\n // Read every entity's coverage snapshot in a single batched query. This endpoint is\n // polled by the status table every few seconds, so the poll path must stay read-cheap:\n // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh\n // event emitted below, never inline per entity.\n const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n\n // An explicit refresh action (?refresh) may block, but only when the durable\n // coverage snapshots are stale. Recent persisted snapshots survive workers/restarts,\n // so repeated refresh requests use them instead of hammering base-table counts.\n if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {\n await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>\n refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),\n )\n const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)\n }\n\n const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)\n\n const jobs = await Promise.all(entityIds.map((eid) => fetchJobSummary(eid, tenantId, organizationId)))\n\n const items: any[] = []\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const eid = entityIds[idx]\n let coverage = coverageSnapshots[idx]\n\n const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null\n const isStale = !coverage || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)\n if (isStale) entitiesNeedingRefresh.add(eid)\n\n const job = jobs[idx]\n const label = (byId.get(eid)?.label) || eid\n const baseCountNumber = normalizeCount(coverage?.baseCount)\n const indexCountNumber = normalizeCount(coverage?.indexedCount)\n const vectorEnabled = vectorEnabledEntities.has(eid)\n const vectorCountNumber = vectorEnabled ? normalizeCount((coverage as any)?.vectorIndexedCount ?? (coverage as any)?.vector_indexed_count) : null\n const fulltextEnabled = fulltextEnabledEntities.has(eid)\n const fulltextCountNumber = fulltextEnabled ? (fulltextEntityCounts?.[eid] ?? 0) : null\n const ok = (() => {\n if (baseCountNumber == null || indexCountNumber == null) return false\n if (baseCountNumber !== indexCountNumber) return false\n if (!vectorEnabled) return true\n return vectorCountNumber != null && vectorCountNumber === baseCountNumber\n })()\n items.push({\n entityId: eid,\n label,\n baseCount: baseCountNumber,\n indexCount: indexCountNumber,\n vectorCount: vectorEnabled ? vectorCountNumber : null,\n vectorEnabled,\n fulltextCount: fulltextCountNumber,\n fulltextEnabled,\n ok,\n job,\n refreshedAt: refreshedAt ?? null,\n })\n }\n\n if (!forceRefresh) {\n try {\n const eventBus = container.resolve('eventBus')\n if (entitiesNeedingRefresh.size > 0) {\n await Promise.all(\n Array.from(entitiesNeedingRefresh).map((entityId) =>\n eventBus\n .emitEvent('query_index.coverage.refresh', {\n entityType: entityId,\n tenantId: tenantId ?? null,\n organizationId,\n delayMs: 0,\n })\n .catch(() => undefined)\n )\n )\n }\n } catch {}\n }\n\n let errorQuery = db\n .selectFrom('indexer_error_logs' as any)\n .selectAll()\n if (tenantId != null) {\n errorQuery = errorQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n errorQuery = errorQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n errorQuery = errorQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n errorQuery = errorQuery.where('organization_id' as any, 'is', null as any)\n }\n const errorRows = await errorQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const errors = errorRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n stack: row.stack ?? null,\n payload: row.payload ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n let logsQuery = db\n .selectFrom('indexer_status_logs' as any)\n .selectAll()\n if (tenantId != null) {\n logsQuery = logsQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n logsQuery = logsQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n logsQuery = logsQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n logsQuery = logsQuery.where('organization_id' as any, 'is', null as any)\n }\n const logRows = await logsQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const logs = logRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n const level = row.level === 'warn' ? 'warn' : 'info'\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n level,\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n details: row.details ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n const response = NextResponse.json({ items, errors, logs })\n const partial = items.find((item) => {\n // Coverage not computed yet (no snapshot) \u2014 pending an async refresh, not a partial\n // index. Do not raise the partial-index warning while counts are still unknown.\n if (item.baseCount == null && item.indexCount == null) return false\n if (item.baseCount == null || item.indexCount == null) return true\n return item.baseCount !== item.indexCount\n })\n if (partial) {\n response.headers.set(\n 'x-om-partial-index',\n JSON.stringify({\n type: 'partial_index',\n entity: partial.entityId,\n entityLabel: partial.label ?? partial.entityId,\n baseCount: partial.baseCount,\n indexedCount: partial.indexCount,\n scope: organizationId,\n })\n )\n }\n return response\n}\n\nconst queryIndexStatusDoc: OpenApiMethodDoc = {\n summary: 'Inspect query index coverage',\n description: 'Returns entity counts comparing base tables with the query index along with the latest job status.',\n tags: [queryIndexTag],\n responses: [\n { status: 200, description: 'Current query index status.', schema: queryIndexStatusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Tenant or organization context required', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Query index status',\n methods: {\n GET: queryIndexStatusDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,7 +7,12 @@ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
7
7
  import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
8
8
  import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
9
9
  import { recordIndexerLog } from "@open-mercato/shared/lib/indexers/status-log";
10
- import { upsertIndexBatch } from "./lib/batch.js";
10
+ import {
11
+ upsertIndexBatch,
12
+ assertIndexBatchWritesLanded,
13
+ createEmptyUpsertIndexBatchResult,
14
+ mergeUpsertIndexBatchResults
15
+ } from "./lib/batch.js";
11
16
  import { reindexEntity, DEFAULT_REINDEX_PARTITIONS } from "./lib/reindexer.js";
12
17
  import { purgeIndexScope } from "./lib/purge.js";
13
18
  import { flattenSystemEntityIds } from "@open-mercato/shared/lib/entities/system-entities";
@@ -136,29 +141,21 @@ async function rebuildEntityIndexes(options) {
136
141
  const encryption = resolveTenantEncryptionService(em);
137
142
  const dekKeyCache = /* @__PURE__ */ new Map();
138
143
  const encryptDoc = async (targetEntity, doc, scope) => {
139
- try {
140
- return await encryptIndexDocForStorage(
141
- targetEntity,
142
- doc,
143
- { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
144
- encryption
145
- );
146
- } catch {
147
- return doc;
148
- }
144
+ return await encryptIndexDocForStorage(
145
+ targetEntity,
146
+ doc,
147
+ { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
148
+ encryption
149
+ );
149
150
  };
150
151
  const decryptDoc = async (targetEntity, doc, scope) => {
151
- try {
152
- return await decryptIndexDocForSearch(
153
- targetEntity,
154
- doc,
155
- { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
156
- encryption,
157
- dekKeyCache
158
- );
159
- } catch {
160
- return doc;
161
- }
152
+ return await decryptIndexDocForSearch(
153
+ targetEntity,
154
+ doc,
155
+ { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
156
+ encryption,
157
+ dekKeyCache
158
+ );
162
159
  };
163
160
  const applyFilters = (q) => {
164
161
  let chain = q;
@@ -181,10 +178,11 @@ async function rebuildEntityIndexes(options) {
181
178
  ).where("id", "=", recordId).executeTakeFirst();
182
179
  if (!row) return { processed: 0, matched: 0 };
183
180
  const bar2 = createProgressBar(progressLabel ?? `Rebuilding ${entityType}`, 1);
184
- await upsertIndexBatch(db, entityType, [row], { orgId: orgOverride, tenantId: tenantOverride }, { encryptDoc, decryptDoc });
185
- bar2.update(1);
181
+ const singleResult = await upsertIndexBatch(db, entityType, [row], { orgId: orgOverride, tenantId: tenantOverride }, { encryptDoc, decryptDoc });
182
+ bar2.update(singleResult.written);
186
183
  bar2.complete();
187
- return { processed: 1, matched: 1 };
184
+ assertIndexBatchWritesLanded(entityType, singleResult);
185
+ return { processed: singleResult.written, matched: 1 };
188
186
  }
189
187
  const countRow = await applyFilters(
190
188
  db.selectFrom(tableName).select(sql`count(*)`.as("count"))
@@ -200,19 +198,23 @@ async function rebuildEntityIndexes(options) {
200
198
  }
201
199
  const bar = createProgressBar(progressLabel ?? `Rebuilding ${entityType}`, intended);
202
200
  let processed = 0;
201
+ let fetched = 0;
203
202
  let cursorOffset = effectiveOffset;
204
203
  let remaining = limitValue;
205
- while (processed < intended) {
204
+ const writeTotals = createEmptyUpsertIndexBatchResult();
205
+ while (fetched < intended) {
206
206
  const chunkLimit = remaining !== void 0 ? Math.min(batchSize, remaining) : batchSize;
207
207
  const chunk = await applyFilters(
208
208
  db.selectFrom(tableName).selectAll().orderBy("id").limit(chunkLimit).offset(cursorOffset)
209
209
  ).execute();
210
210
  if (!chunk.length) break;
211
- await upsertIndexBatch(db, entityType, chunk, {
211
+ const chunkResult = await upsertIndexBatch(db, entityType, chunk, {
212
212
  orgId: orgOverride,
213
213
  tenantId: tenantOverride
214
214
  }, { encryptDoc, decryptDoc });
215
- processed += chunk.length;
215
+ mergeUpsertIndexBatchResults(writeTotals, chunkResult);
216
+ processed += chunkResult.written;
217
+ fetched += chunk.length;
216
218
  cursorOffset += chunk.length;
217
219
  if (remaining !== void 0) remaining -= chunk.length;
218
220
  bar.update(processed);
@@ -222,6 +224,7 @@ async function rebuildEntityIndexes(options) {
222
224
  bar.update(processed);
223
225
  }
224
226
  bar.complete();
227
+ assertIndexBatchWritesLanded(entityType, writeTotals);
225
228
  return { processed, matched: intended };
226
229
  }
227
230
  async function getColumnSet(db, tableName) {