@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
@@ -10,7 +10,7 @@ export const queryIndexErrorSchema = z.object({
10
10
  export const queryIndexPartitionSchema = z.object({
11
11
  partitionIndex: z.number().int().nonnegative().nullable().optional(),
12
12
  partitionCount: z.number().int().positive().nullable().optional(),
13
- status: z.enum(['reindexing', 'purging', 'stalled', 'completed']),
13
+ status: z.enum(['reindexing', 'purging', 'stalled', 'completed', 'failed']),
14
14
  startedAt: z.string().nullable().optional(),
15
15
  finishedAt: z.string().nullable().optional(),
16
16
  heartbeatAt: z.string().nullable().optional(),
@@ -19,7 +19,7 @@ export const queryIndexPartitionSchema = z.object({
19
19
  })
20
20
 
21
21
  export const queryIndexJobSchema = z.object({
22
- status: z.enum(['idle', 'reindexing', 'purging', 'stalled']),
22
+ status: z.enum(['idle', 'reindexing', 'purging', 'stalled', 'failed']),
23
23
  startedAt: z.string().nullable().optional(),
24
24
  finishedAt: z.string().nullable().optional(),
25
25
  heartbeatAt: z.string().nullable().optional(),
@@ -240,7 +240,7 @@ export async function GET(req: Request) {
240
240
  const stalled =
241
241
  !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)
242
242
  const state = finishedDate
243
- ? 'completed'
243
+ ? (row.status === 'failed' ? 'failed' : 'completed')
244
244
  : stalled
245
245
  ? 'stalled'
246
246
  : (row.status as string) || 'reindexing'
@@ -261,22 +261,31 @@ export async function GET(req: Request) {
261
261
  (p) => p.status === 'reindexing' || p.status === 'purging',
262
262
  )
263
263
  const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')
264
- let status: 'idle' | 'reindexing' | 'purging' | 'stalled' = 'idle'
264
+ const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null
265
+ let status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed' = 'idle'
265
266
  if (activePartitions.length) {
266
267
  if (runningPartitions.length) {
267
268
  status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'
268
269
  } else if (stalledPartitions.length) {
269
270
  status = 'stalled'
270
271
  }
272
+ } else if (
273
+ partitions.some((p) => p.status === 'failed')
274
+ || (scopeCandidate?.row.finished_at && scopeCandidate.row.status === 'failed')
275
+ ) {
276
+ // The run finished but lost records; without this it reports "idle" and the only
277
+ // hint that anything went wrong is the coverage percentage.
278
+ status = 'failed'
271
279
  }
272
280
 
273
281
  const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null
274
- const finishedAt = status === 'idle' ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null) : null
282
+ const finishedAt = status === 'idle' || status === 'failed'
283
+ ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null)
284
+ : null
275
285
  const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null
276
286
  const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)
277
287
  const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)
278
288
  const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null
279
- const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null
280
289
 
281
290
  return {
282
291
  status,
@@ -291,7 +300,7 @@ export async function GET(req: Request) {
291
300
  status: (() => {
292
301
  const heartbeatDate = scopeCandidate!.row.heartbeat_at ? new Date(scopeCandidate!.row.heartbeat_at) : null
293
302
  const finishedDate = scopeCandidate!.row.finished_at ? new Date(scopeCandidate!.row.finished_at) : null
294
- if (finishedDate) return 'completed'
303
+ if (finishedDate) return scopeCandidate!.row.status === 'failed' ? 'failed' : 'completed'
295
304
  if (
296
305
  !heartbeatDate ||
297
306
  Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS
@@ -14,7 +14,13 @@ type ProgressBarHandle = {
14
14
  import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
15
15
  import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
16
16
  import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
17
- import { upsertIndexBatch, type AnyRow } from './lib/batch'
17
+ import {
18
+ upsertIndexBatch,
19
+ assertIndexBatchWritesLanded,
20
+ createEmptyUpsertIndexBatchResult,
21
+ mergeUpsertIndexBatchResults,
22
+ type AnyRow,
23
+ } from './lib/batch'
18
24
  import { reindexEntity, DEFAULT_REINDEX_PARTITIONS } from './lib/reindexer'
19
25
  import { purgeIndexScope } from './lib/purge'
20
26
  import { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'
@@ -185,21 +191,20 @@ async function rebuildEntityIndexes(options: RebuildExecutionOptions): Promise<R
185
191
  const encryption = resolveTenantEncryptionService(em as any)
186
192
  const dekKeyCache = new Map<string | null, string | null>()
187
193
 
194
+ // Neither helper swallows failures: upsertIndexBatch treats a failed encrypt as a
195
+ // failed row (writing the plaintext document instead would break encryption at rest)
196
+ // and records a failed decrypt against fulltext.
188
197
  const encryptDoc = async (
189
198
  targetEntity: string,
190
199
  doc: Record<string, unknown>,
191
200
  scope: { organizationId: string | null; tenantId: string | null },
192
201
  ) => {
193
- try {
194
- return await encryptIndexDocForStorage(
195
- targetEntity,
196
- doc,
197
- { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
198
- encryption,
199
- )
200
- } catch {
201
- return doc
202
- }
202
+ return await encryptIndexDocForStorage(
203
+ targetEntity,
204
+ doc,
205
+ { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
206
+ encryption,
207
+ )
203
208
  }
204
209
 
205
210
  const decryptDoc = async (
@@ -207,17 +212,13 @@ async function rebuildEntityIndexes(options: RebuildExecutionOptions): Promise<R
207
212
  doc: Record<string, unknown>,
208
213
  scope: { organizationId: string | null; tenantId: string | null },
209
214
  ) => {
210
- try {
211
- return await decryptIndexDocForSearch(
212
- targetEntity,
213
- doc,
214
- { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
215
- encryption,
216
- dekKeyCache,
217
- )
218
- } catch {
219
- return doc
220
- }
215
+ return await decryptIndexDocForSearch(
216
+ targetEntity,
217
+ doc,
218
+ { tenantId: scope.tenantId ?? null, organizationId: scope.organizationId ?? null },
219
+ encryption,
220
+ dekKeyCache,
221
+ )
221
222
  }
222
223
 
223
224
  const applyFilters = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {
@@ -244,10 +245,11 @@ async function rebuildEntityIndexes(options: RebuildExecutionOptions): Promise<R
244
245
  .executeTakeFirst() as AnyRow | undefined
245
246
  if (!row) return { processed: 0, matched: 0 }
246
247
  const bar = createProgressBar(progressLabel ?? `Rebuilding ${entityType}`, 1)
247
- await upsertIndexBatch(db, entityType, [row], { orgId: orgOverride, tenantId: tenantOverride }, { encryptDoc, decryptDoc })
248
- bar.update(1)
248
+ const singleResult = await upsertIndexBatch(db, entityType, [row], { orgId: orgOverride, tenantId: tenantOverride }, { encryptDoc, decryptDoc })
249
+ bar.update(singleResult.written)
249
250
  bar.complete()
250
- return { processed: 1, matched: 1 }
251
+ assertIndexBatchWritesLanded(entityType, singleResult)
252
+ return { processed: singleResult.written, matched: 1 }
251
253
  }
252
254
 
253
255
  const countRow = await applyFilters(
@@ -265,10 +267,12 @@ async function rebuildEntityIndexes(options: RebuildExecutionOptions): Promise<R
265
267
 
266
268
  const bar = createProgressBar(progressLabel ?? `Rebuilding ${entityType}`, intended)
267
269
  let processed = 0
270
+ let fetched = 0
268
271
  let cursorOffset = effectiveOffset
269
272
  let remaining = limitValue
273
+ const writeTotals = createEmptyUpsertIndexBatchResult()
270
274
 
271
- while (processed < intended) {
275
+ while (fetched < intended) {
272
276
  const chunkLimit = remaining !== undefined ? Math.min(batchSize, remaining) : batchSize
273
277
  const chunk = await applyFilters(
274
278
  db
@@ -280,12 +284,14 @@ async function rebuildEntityIndexes(options: RebuildExecutionOptions): Promise<R
280
284
  ).execute() as AnyRow[]
281
285
  if (!chunk.length) break
282
286
 
283
- await upsertIndexBatch(db, entityType, chunk as AnyRow[], {
287
+ const chunkResult = await upsertIndexBatch(db, entityType, chunk as AnyRow[], {
284
288
  orgId: orgOverride,
285
289
  tenantId: tenantOverride,
286
290
  }, { encryptDoc, decryptDoc })
291
+ mergeUpsertIndexBatchResults(writeTotals, chunkResult)
287
292
 
288
- processed += chunk.length
293
+ processed += chunkResult.written
294
+ fetched += chunk.length
289
295
  cursorOffset += chunk.length
290
296
  if (remaining !== undefined) remaining -= chunk.length
291
297
  bar.update(processed)
@@ -296,6 +302,8 @@ async function rebuildEntityIndexes(options: RebuildExecutionOptions): Promise<R
296
302
  bar.update(processed)
297
303
  }
298
304
  bar.complete()
305
+ // After bar.complete() so the terminal is left in a sane state before we throw.
306
+ assertIndexBatchWritesLanded(entityType, writeTotals)
299
307
  return { processed, matched: intended }
300
308
  }
301
309
 
@@ -23,7 +23,7 @@ type Translator = (key: string, params?: Record<string, string | number>) => str
23
23
  type PartitionStatus = {
24
24
  partitionIndex: number | null
25
25
  partitionCount: number | null
26
- status: 'reindexing' | 'purging' | 'stalled' | 'completed'
26
+ status: 'reindexing' | 'purging' | 'stalled' | 'completed' | 'failed'
27
27
  processedCount?: number | null
28
28
  totalCount?: number | null
29
29
  heartbeatAt?: string | null
@@ -32,7 +32,7 @@ type PartitionStatus = {
32
32
  }
33
33
 
34
34
  type JobStatus = {
35
- status: 'idle' | 'reindexing' | 'purging' | 'stalled'
35
+ status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed'
36
36
  startedAt?: string | null
37
37
  finishedAt?: string | null
38
38
  heartbeatAt?: string | null
@@ -40,7 +40,7 @@ type JobStatus = {
40
40
  totalCount?: number | null
41
41
  partitions?: PartitionStatus[]
42
42
  scope?: {
43
- status?: 'reindexing' | 'purging' | 'stalled' | 'completed' | null
43
+ status?: 'reindexing' | 'purging' | 'stalled' | 'completed' | 'failed' | null
44
44
  processedCount?: number | null
45
45
  totalCount?: number | null
46
46
  } | null
@@ -90,6 +90,7 @@ function translateJobStatus(t: Translator, status: JobStatus['status'] | undefin
90
90
  if (status === 'reindexing') return t('query_index.table.status.reindexing')
91
91
  if (status === 'purging') return t('query_index.table.status.purging')
92
92
  if (status === 'stalled') return t('query_index.table.status.stalled')
93
+ if (status === 'failed') return t('query_index.table.status.failed')
93
94
  return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')
94
95
  }
95
96
 
@@ -100,6 +101,7 @@ function translateScopeStatus(
100
101
  if (status === 'reindexing') return t('query_index.table.status.scope.reindexing')
101
102
  if (status === 'purging') return t('query_index.table.status.scope.purging')
102
103
  if (status === 'stalled') return t('query_index.table.status.scope.stalled')
104
+ if (status === 'failed') return t('query_index.table.status.scope.failed')
103
105
  return t('query_index.table.status.scope.completed')
104
106
  }
105
107
 
@@ -158,6 +160,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
158
160
  const job = record.job
159
161
  const partitions = job?.partitions ?? []
160
162
  const ok = record.ok && (!job || job.status === 'idle')
163
+ const jobFailed = job?.status === 'failed'
161
164
  const statusText = translateJobStatus(t, job?.status, ok)
162
165
  const jobProgress = job ? formatProgressLabel(job.processedCount ?? null, job.totalCount ?? null, t) : null
163
166
  const label = jobProgress
@@ -165,7 +168,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
165
168
  : statusText
166
169
  let variant: StatusBadgeVariant = 'neutral'
167
170
  if (job) {
168
- if (job.status === 'stalled') variant = 'error'
171
+ if (job.status === 'stalled' || jobFailed) variant = 'error'
169
172
  else if (job.status === 'reindexing' || job.status === 'purging') variant = 'warning'
170
173
  else variant = ok ? 'success' : 'neutral'
171
174
  } else {
@@ -46,6 +46,7 @@
46
46
  "query_index.table.logs.viewDetails": "Details anzeigen",
47
47
  "query_index.table.refresh": "Aktualisieren",
48
48
  "query_index.table.searchPlaceholder": "Nach Entität filtern",
49
+ "query_index.table.status.failed": "Fehlgeschlagen",
49
50
  "query_index.table.status.in_sync": "Synchron",
50
51
  "query_index.table.status.out_of_sync": "Nicht synchron",
51
52
  "query_index.table.status.partitionLabel": "Partition {{index}}",
@@ -54,6 +55,7 @@
54
55
  "query_index.table.status.purging": "Bereinigung",
55
56
  "query_index.table.status.reindexing": "Reindizierung",
56
57
  "query_index.table.status.scope.completed": "Abgeschlossen",
58
+ "query_index.table.status.scope.failed": "Fehlgeschlagen",
57
59
  "query_index.table.status.scope.purging": "Bereinigung",
58
60
  "query_index.table.status.scope.reindexing": "Läuft",
59
61
  "query_index.table.status.scope.stalled": "Angehalten",
@@ -46,6 +46,7 @@
46
46
  "query_index.table.logs.viewDetails": "View details",
47
47
  "query_index.table.refresh": "Refresh",
48
48
  "query_index.table.searchPlaceholder": "Filter by entity",
49
+ "query_index.table.status.failed": "Failed",
49
50
  "query_index.table.status.in_sync": "In sync",
50
51
  "query_index.table.status.out_of_sync": "Out of sync",
51
52
  "query_index.table.status.partitionLabel": "Partition {{index}}",
@@ -54,6 +55,7 @@
54
55
  "query_index.table.status.purging": "Purging",
55
56
  "query_index.table.status.reindexing": "Reindexing",
56
57
  "query_index.table.status.scope.completed": "Done",
58
+ "query_index.table.status.scope.failed": "Failed",
57
59
  "query_index.table.status.scope.purging": "Purging",
58
60
  "query_index.table.status.scope.reindexing": "Running",
59
61
  "query_index.table.status.scope.stalled": "Stalled",
@@ -46,6 +46,7 @@
46
46
  "query_index.table.logs.viewDetails": "Ver detalles",
47
47
  "query_index.table.refresh": "Actualizar",
48
48
  "query_index.table.searchPlaceholder": "Filtrar por entidad",
49
+ "query_index.table.status.failed": "Con errores",
49
50
  "query_index.table.status.in_sync": "Sincronizado",
50
51
  "query_index.table.status.out_of_sync": "Fuera de sincronización",
51
52
  "query_index.table.status.partitionLabel": "Partición {{index}}",
@@ -54,6 +55,7 @@
54
55
  "query_index.table.status.purging": "Depurando",
55
56
  "query_index.table.status.reindexing": "Reindexando",
56
57
  "query_index.table.status.scope.completed": "Completado",
58
+ "query_index.table.status.scope.failed": "Con errores",
57
59
  "query_index.table.status.scope.purging": "Depurando",
58
60
  "query_index.table.status.scope.reindexing": "Ejecutando",
59
61
  "query_index.table.status.scope.stalled": "Detenido",
@@ -46,6 +46,7 @@
46
46
  "query_index.table.logs.viewDetails": "Pokaż szczegóły",
47
47
  "query_index.table.refresh": "Odśwież",
48
48
  "query_index.table.searchPlaceholder": "Filtruj według encji",
49
+ "query_index.table.status.failed": "Niepowodzenie",
49
50
  "query_index.table.status.in_sync": "Zsynchronizowany",
50
51
  "query_index.table.status.out_of_sync": "Poza synchronizacją",
51
52
  "query_index.table.status.partitionLabel": "Partycja {{index}}",
@@ -54,6 +55,7 @@
54
55
  "query_index.table.status.purging": "Czyszczenie",
55
56
  "query_index.table.status.reindexing": "Ponowne indeksowanie",
56
57
  "query_index.table.status.scope.completed": "Zakończone",
58
+ "query_index.table.status.scope.failed": "Niepowodzenie",
57
59
  "query_index.table.status.scope.purging": "Czyszczenie",
58
60
  "query_index.table.status.scope.reindexing": "Uruchomione",
59
61
  "query_index.table.status.scope.stalled": "Zatrzymane",