@open-mercato/core 0.4.7-develop-78d7541539 → 0.4.7-develop-74069040de

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 (187) hide show
  1. package/AGENTS.md +1 -0
  2. package/dist/modules/catalog/api/bulk-delete/route.js +86 -0
  3. package/dist/modules/catalog/api/bulk-delete/route.js.map +7 -0
  4. package/dist/modules/catalog/api/prices/route.js +39 -6
  5. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  6. package/dist/modules/catalog/api/products/route.js +6 -11
  7. package/dist/modules/catalog/api/products/route.js.map +2 -2
  8. package/dist/modules/catalog/commands/products.js +2 -0
  9. package/dist/modules/catalog/commands/products.js.map +2 -2
  10. package/dist/modules/catalog/components/products/ProductsDataTable.js +9 -1
  11. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  12. package/dist/modules/catalog/lib/bulkDelete.js +70 -0
  13. package/dist/modules/catalog/lib/bulkDelete.js.map +7 -0
  14. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js +185 -0
  15. package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js.map +7 -0
  16. package/dist/modules/catalog/widgets/injection-table.js +9 -1
  17. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  18. package/dist/modules/catalog/workers/catalog-product-bulk-delete.js +40 -0
  19. package/dist/modules/catalog/workers/catalog-product-bulk-delete.js.map +7 -0
  20. package/dist/modules/data_sync/api/options.js +52 -0
  21. package/dist/modules/data_sync/api/options.js.map +7 -0
  22. package/dist/modules/data_sync/api/run.js +30 -35
  23. package/dist/modules/data_sync/api/run.js.map +2 -2
  24. package/dist/modules/data_sync/api/runs/[id]/cancel.js +2 -2
  25. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  26. package/dist/modules/data_sync/api/runs/[id]/retry.js +15 -30
  27. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  28. package/dist/modules/data_sync/api/schedules/[id]/route.js +109 -0
  29. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +7 -0
  30. package/dist/modules/data_sync/api/schedules/route.js +72 -0
  31. package/dist/modules/data_sync/api/schedules/route.js.map +7 -0
  32. package/dist/modules/data_sync/api/schedules/serialize.js +21 -0
  33. package/dist/modules/data_sync/api/schedules/serialize.js.map +7 -0
  34. package/dist/modules/data_sync/backend/data-sync/page.js +656 -47
  35. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  36. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +116 -34
  37. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  38. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +394 -0
  39. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +7 -0
  40. package/dist/modules/data_sync/data/validators.js +32 -0
  41. package/dist/modules/data_sync/data/validators.js.map +2 -2
  42. package/dist/modules/data_sync/di.js +2 -0
  43. package/dist/modules/data_sync/di.js.map +2 -2
  44. package/dist/modules/data_sync/lib/id-mapping.js +24 -2
  45. package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
  46. package/dist/modules/data_sync/lib/start-run.js +57 -0
  47. package/dist/modules/data_sync/lib/start-run.js.map +7 -0
  48. package/dist/modules/data_sync/lib/sync-engine.js +93 -4
  49. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  50. package/dist/modules/data_sync/lib/sync-run-service.js +5 -1
  51. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  52. package/dist/modules/data_sync/lib/sync-schedule-service.js +138 -0
  53. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +7 -0
  54. package/dist/modules/data_sync/workers/sync-export.js +28 -2
  55. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  56. package/dist/modules/data_sync/workers/sync-import.js +28 -2
  57. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  58. package/dist/modules/data_sync/workers/sync-scheduled.js +5 -0
  59. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  60. package/dist/modules/entities/api/definitions.js +5 -2
  61. package/dist/modules/entities/api/definitions.js.map +2 -2
  62. package/dist/modules/entities/lib/field-definitions.js +3 -1
  63. package/dist/modules/entities/lib/field-definitions.js.map +2 -2
  64. package/dist/modules/integrations/api/[id]/route.js +14 -15
  65. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  66. package/dist/modules/integrations/api/route.js +3 -3
  67. package/dist/modules/integrations/api/route.js.map +2 -2
  68. package/dist/modules/integrations/backend/integrations/[id]/page.js +148 -33
  69. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  70. package/dist/modules/integrations/lib/state-service.js +15 -1
  71. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  72. package/dist/modules/messages/api/[id]/route.js +24 -22
  73. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  74. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  75. package/dist/modules/progress/api/active/route.js +3 -1
  76. package/dist/modules/progress/api/active/route.js.map +2 -2
  77. package/dist/modules/progress/api/jobs/[id]/route.js +1 -1
  78. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  79. package/dist/modules/progress/api/jobs/route.js +1 -1
  80. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  81. package/dist/modules/progress/lib/events.js.map +1 -1
  82. package/dist/modules/progress/lib/progressService.js.map +2 -2
  83. package/dist/modules/progress/lib/progressServiceImpl.js +42 -1
  84. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  85. package/dist/modules/query_index/lib/document.js +35 -1
  86. package/dist/modules/query_index/lib/document.js.map +2 -2
  87. package/dist/modules/query_index/lib/engine.js +91 -4
  88. package/dist/modules/query_index/lib/engine.js.map +2 -2
  89. package/dist/modules/query_index/lib/indexer.js +2 -0
  90. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  91. package/dist/modules/sales/api/adjustment-kinds/route.js +3 -9
  92. package/dist/modules/sales/api/adjustment-kinds/route.js.map +2 -2
  93. package/dist/modules/sales/api/channels/route.js +3 -10
  94. package/dist/modules/sales/api/channels/route.js.map +2 -2
  95. package/dist/modules/sales/api/delivery-windows/route.js +3 -10
  96. package/dist/modules/sales/api/delivery-windows/route.js.map +2 -2
  97. package/dist/modules/sales/api/payment-methods/route.js +3 -11
  98. package/dist/modules/sales/api/payment-methods/route.js.map +2 -2
  99. package/dist/modules/sales/api/price-kinds/route.js +3 -5
  100. package/dist/modules/sales/api/price-kinds/route.js.map +2 -2
  101. package/dist/modules/sales/api/shipping-methods/route.js +3 -11
  102. package/dist/modules/sales/api/shipping-methods/route.js.map +2 -2
  103. package/dist/modules/sales/api/tags/route.js +3 -9
  104. package/dist/modules/sales/api/tags/route.js.map +2 -2
  105. package/dist/modules/sales/api/tax-rates/route.js +3 -13
  106. package/dist/modules/sales/api/tax-rates/route.js.map +2 -2
  107. package/dist/modules/sales/api/utils.js +9 -0
  108. package/dist/modules/sales/api/utils.js.map +2 -2
  109. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +3 -9
  110. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +2 -2
  111. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -2
  112. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  113. package/dist/modules/workflows/api/definitions/route.js +4 -3
  114. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  115. package/dist/modules/workflows/api/definitions/serialize.js +25 -0
  116. package/dist/modules/workflows/api/definitions/serialize.js.map +7 -0
  117. package/package.json +3 -3
  118. package/src/modules/catalog/api/bulk-delete/route.ts +93 -0
  119. package/src/modules/catalog/api/prices/route.ts +53 -6
  120. package/src/modules/catalog/api/products/route.ts +6 -11
  121. package/src/modules/catalog/commands/products.ts +2 -0
  122. package/src/modules/catalog/components/products/ProductsDataTable.tsx +8 -0
  123. package/src/modules/catalog/i18n/de.json +10 -0
  124. package/src/modules/catalog/i18n/en.json +10 -0
  125. package/src/modules/catalog/i18n/es.json +10 -0
  126. package/src/modules/catalog/i18n/pl.json +10 -0
  127. package/src/modules/catalog/lib/bulkDelete.ts +106 -0
  128. package/src/modules/catalog/widgets/injection/product-bulk-delete/widget.ts +242 -0
  129. package/src/modules/catalog/widgets/injection-table.ts +8 -0
  130. package/src/modules/catalog/workers/catalog-product-bulk-delete.ts +48 -0
  131. package/src/modules/data_sync/AGENTS.md +11 -3
  132. package/src/modules/data_sync/api/options.ts +58 -0
  133. package/src/modules/data_sync/api/run.ts +34 -36
  134. package/src/modules/data_sync/api/runs/[id]/cancel.ts +2 -2
  135. package/src/modules/data_sync/api/runs/[id]/retry.ts +14 -31
  136. package/src/modules/data_sync/api/schedules/[id]/route.ts +130 -0
  137. package/src/modules/data_sync/api/schedules/route.ts +77 -0
  138. package/src/modules/data_sync/api/schedules/serialize.ts +31 -0
  139. package/src/modules/data_sync/backend/data-sync/page.tsx +756 -2
  140. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +179 -53
  141. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +512 -0
  142. package/src/modules/data_sync/data/validators.ts +35 -0
  143. package/src/modules/data_sync/di.ts +6 -0
  144. package/src/modules/data_sync/i18n/de.json +72 -0
  145. package/src/modules/data_sync/i18n/en.json +72 -0
  146. package/src/modules/data_sync/i18n/es.json +72 -0
  147. package/src/modules/data_sync/i18n/pl.json +72 -0
  148. package/src/modules/data_sync/lib/adapter.ts +4 -1
  149. package/src/modules/data_sync/lib/id-mapping.ts +32 -2
  150. package/src/modules/data_sync/lib/start-run.ts +90 -0
  151. package/src/modules/data_sync/lib/sync-engine.ts +111 -4
  152. package/src/modules/data_sync/lib/sync-run-service.ts +5 -1
  153. package/src/modules/data_sync/lib/sync-schedule-service.ts +207 -0
  154. package/src/modules/data_sync/workers/sync-export.ts +33 -2
  155. package/src/modules/data_sync/workers/sync-import.ts +33 -2
  156. package/src/modules/data_sync/workers/sync-scheduled.ts +7 -0
  157. package/src/modules/entities/api/definitions.ts +12 -2
  158. package/src/modules/entities/lib/field-definitions.ts +2 -0
  159. package/src/modules/integrations/AGENTS.md +16 -3
  160. package/src/modules/integrations/api/[id]/route.ts +14 -15
  161. package/src/modules/integrations/api/route.ts +3 -3
  162. package/src/modules/integrations/backend/integrations/[id]/page.tsx +176 -54
  163. package/src/modules/integrations/lib/state-service.ts +25 -1
  164. package/src/modules/messages/api/[id]/route.ts +25 -22
  165. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +3 -3
  166. package/src/modules/progress/api/active/route.ts +4 -1
  167. package/src/modules/progress/api/jobs/[id]/route.ts +1 -1
  168. package/src/modules/progress/api/jobs/route.ts +1 -1
  169. package/src/modules/progress/lib/events.ts +6 -0
  170. package/src/modules/progress/lib/progressService.ts +1 -0
  171. package/src/modules/progress/lib/progressServiceImpl.ts +47 -1
  172. package/src/modules/query_index/lib/document.ts +52 -1
  173. package/src/modules/query_index/lib/engine.ts +104 -4
  174. package/src/modules/query_index/lib/indexer.ts +2 -0
  175. package/src/modules/sales/api/adjustment-kinds/route.ts +3 -9
  176. package/src/modules/sales/api/channels/route.ts +3 -10
  177. package/src/modules/sales/api/delivery-windows/route.ts +3 -10
  178. package/src/modules/sales/api/payment-methods/route.ts +3 -11
  179. package/src/modules/sales/api/price-kinds/route.ts +3 -5
  180. package/src/modules/sales/api/shipping-methods/route.ts +3 -11
  181. package/src/modules/sales/api/tags/route.ts +3 -9
  182. package/src/modules/sales/api/tax-rates/route.ts +3 -13
  183. package/src/modules/sales/api/utils.ts +9 -0
  184. package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +3 -9
  185. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -2
  186. package/src/modules/workflows/api/definitions/route.ts +4 -3
  187. package/src/modules/workflows/api/definitions/serialize.ts +23 -0
@@ -16,6 +16,7 @@ function buildJobPayload(job: ProgressJob): Record<string, unknown> {
16
16
  totalCount: job.totalCount ?? null,
17
17
  etaSeconds: job.etaSeconds ?? null,
18
18
  cancellable: job.cancellable,
19
+ meta: job.meta ?? null,
19
20
  startedAt: job.startedAt?.toISOString() ?? null,
20
21
  finishedAt: job.finishedAt?.toISOString() ?? null,
21
22
  }
@@ -53,6 +54,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
53
54
 
54
55
  async startJob(jobId, ctx) {
55
56
  const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
57
+ if (job.status === 'cancelled') {
58
+ return job
59
+ }
56
60
 
57
61
  job.status = 'running'
58
62
  job.startedAt = new Date()
@@ -71,6 +75,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
71
75
 
72
76
  async updateProgress(jobId, input, ctx) {
73
77
  const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
78
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
79
+ return job
80
+ }
74
81
 
75
82
  if (input.processedCount !== undefined) {
76
83
  job.processedCount = input.processedCount
@@ -109,6 +116,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
109
116
 
110
117
  async incrementProgress(jobId, delta, ctx) {
111
118
  const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
119
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
120
+ return job
121
+ }
112
122
 
113
123
  job.processedCount += delta
114
124
  job.heartbeatAt = new Date()
@@ -134,6 +144,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
134
144
  async completeJob(jobId, input, ctx) {
135
145
  const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
136
146
  if (!job) throw new Error(`Job ${jobId} not found`)
147
+ if (job.status === 'cancelled') {
148
+ return job
149
+ }
137
150
 
138
151
  job.status = 'completed'
139
152
  job.finishedAt = new Date()
@@ -158,6 +171,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
158
171
  async failJob(jobId, input, ctx) {
159
172
  const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
160
173
  if (!job) throw new Error(`Job ${jobId} not found`)
174
+ if (job.status === 'cancelled') {
175
+ return job
176
+ }
161
177
 
162
178
  job.status = 'failed'
163
179
  job.finishedAt = new Date()
@@ -203,6 +219,30 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
203
219
  return job
204
220
  },
205
221
 
222
+ async markCancelled(jobId, ctx) {
223
+ const job = await em.findOne(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
224
+ if (!job) throw new Error(`Job ${jobId} not found`)
225
+ if (job.status === 'cancelled') {
226
+ return job
227
+ }
228
+
229
+ job.cancelRequestedAt = job.cancelRequestedAt ?? new Date()
230
+ job.cancelledByUserId = ctx.userId
231
+ job.status = 'cancelled'
232
+ job.finishedAt = job.finishedAt ?? new Date()
233
+ job.etaSeconds = 0
234
+
235
+ await em.flush()
236
+
237
+ await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {
238
+ ...buildJobPayload(job),
239
+ tenantId: ctx.tenantId,
240
+ organizationId: job.organizationId ?? null,
241
+ })
242
+
243
+ return job
244
+ },
245
+
206
246
  async isCancellationRequested(jobId) {
207
247
  const job = await em.findOne(ProgressJob, { id: jobId })
208
248
  return job?.cancelRequestedAt != null
@@ -247,7 +287,13 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
247
287
  const staleJobs = await em.find(ProgressJob, {
248
288
  tenantId,
249
289
  status: 'running',
250
- heartbeatAt: { $lt: cutoff },
290
+ $or: [
291
+ { heartbeatAt: { $lt: cutoff } },
292
+ {
293
+ heartbeatAt: null,
294
+ startedAt: { $lt: cutoff },
295
+ },
296
+ ],
251
297
  })
252
298
 
253
299
  for (const job of staleJobs) {
@@ -10,6 +10,8 @@ export type IndexCustomFieldValue = {
10
10
  tenantId?: string | null
11
11
  }
12
12
 
13
+ export const AGGREGATE_SEARCH_FIELD = 'search_text'
14
+
13
15
  function normalizeScopeValue(value: string | null | undefined): string | null {
14
16
  if (value === undefined || value === null || value === '') return null
15
17
  return value
@@ -28,6 +30,55 @@ function normalizeValue(value: unknown): unknown {
28
30
  return value
29
31
  }
30
32
 
33
+ function collectAggregateSearchValues(field: string, value: unknown): string[] {
34
+ const lower = field.toLowerCase()
35
+ if (
36
+ lower === AGGREGATE_SEARCH_FIELD
37
+ || lower === 'id'
38
+ || lower.endsWith('_id')
39
+ || lower.endsWith('.id')
40
+ || lower.endsWith('_at')
41
+ || ['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)
42
+ ) {
43
+ return []
44
+ }
45
+
46
+ if (typeof value === 'string') {
47
+ const trimmed = value.trim()
48
+ return trimmed.length > 0 ? [trimmed] : []
49
+ }
50
+
51
+ if (Array.isArray(value)) {
52
+ return value
53
+ .filter((entry): entry is string => typeof entry === 'string')
54
+ .map((entry) => entry.trim())
55
+ .filter((entry) => entry.length > 0)
56
+ }
57
+
58
+ return []
59
+ }
60
+
61
+ export function attachAggregateSearchField(doc: Record<string, unknown>): Record<string, unknown> {
62
+ const parts: string[] = []
63
+ const seen = new Set<string>()
64
+
65
+ for (const [field, value] of Object.entries(doc)) {
66
+ const values = collectAggregateSearchValues(field, value)
67
+ for (const entry of values) {
68
+ const key = entry.toLowerCase()
69
+ if (seen.has(key)) continue
70
+ seen.add(key)
71
+ parts.push(entry)
72
+ }
73
+ }
74
+
75
+ if (parts.length > 0) {
76
+ doc[AGGREGATE_SEARCH_FIELD] = parts.join('\n')
77
+ }
78
+
79
+ return doc
80
+ }
81
+
31
82
  export function buildIndexDocument(
32
83
  baseRow: Record<string, unknown>,
33
84
  customFieldValues: Iterable<IndexCustomFieldValue> = [],
@@ -72,5 +123,5 @@ export function buildIndexDocument(
72
123
  }
73
124
  }
74
125
 
75
- return doc
126
+ return attachAggregateSearchField(doc)
76
127
  }
@@ -561,14 +561,41 @@ export class HybridQueryEngine implements QueryEngine {
561
561
  }
562
562
 
563
563
  for (const filter of baseFilters) {
564
- const baseField = resolveBaseColumn(String(filter.field))
565
- if (!baseField) continue
564
+ const fieldName = String(filter.field)
565
+ const baseField = resolveBaseColumn(fieldName)
566
+ if (!baseField) {
567
+ builder = this.applyIndexDocFilterFromAlias(
568
+ knex,
569
+ builder,
570
+ 'ei',
571
+ entity,
572
+ fieldName,
573
+ filter.op,
574
+ filter.value,
575
+ 'b.id',
576
+ searchRuntime,
577
+ )
578
+ if (optimizedCountBuilder) {
579
+ optimizedCountBuilder = this.applyIndexDocFilterFromAlias(
580
+ knex,
581
+ optimizedCountBuilder,
582
+ 'ei',
583
+ entity,
584
+ fieldName,
585
+ filter.op,
586
+ filter.value,
587
+ 'b.id',
588
+ searchRuntime,
589
+ )
590
+ }
591
+ continue
592
+ }
566
593
  const column = qualify(baseField)
567
594
  builder = this.applyColumnFilter(builder, column, filter, {
568
595
  ...searchRuntime,
569
596
  knex,
570
597
  entity,
571
- field: String(filter.field),
598
+ field: fieldName,
572
599
  recordIdColumn: 'b.id',
573
600
  })
574
601
  if (optimizedCountBuilder) {
@@ -576,7 +603,7 @@ export class HybridQueryEngine implements QueryEngine {
576
603
  ...searchRuntime,
577
604
  knex,
578
605
  entity,
579
- field: String(filter.field),
606
+ field: fieldName,
580
607
  recordIdColumn: 'b.id',
581
608
  })
582
609
  }
@@ -1141,6 +1168,79 @@ export class HybridQueryEngine implements QueryEngine {
1141
1168
  }
1142
1169
  }
1143
1170
 
1171
+ private applyIndexDocFilterFromAlias(
1172
+ knex: Knex,
1173
+ q: ResultBuilder,
1174
+ alias: string,
1175
+ entityType: string,
1176
+ key: string,
1177
+ op: FilterOp,
1178
+ value: unknown,
1179
+ recordIdColumn: string,
1180
+ search?: SearchRuntime,
1181
+ ): ResultBuilder {
1182
+ const text = knex.raw(`(${alias}.doc ->> ?)`, [key])
1183
+ if ((op === 'like' || op === 'ilike') && search?.enabled && typeof value === 'string') {
1184
+ const tokens = tokenizeText(String(value), search.config)
1185
+ const hashes = tokens.hashes
1186
+ if (hashes.length) {
1187
+ const applied = this.applySearchTokens(q, {
1188
+ knex,
1189
+ entity: entityType,
1190
+ field: key,
1191
+ hashes,
1192
+ recordIdColumn,
1193
+ tenantId: search.tenantId ?? null,
1194
+ organizationScope: search.organizationScope ?? null,
1195
+ })
1196
+ this.logSearchDebug('search:index-doc-filter', {
1197
+ entity: entityType,
1198
+ field: key,
1199
+ tokens: tokens.tokens,
1200
+ hashes,
1201
+ applied,
1202
+ tenantId: search.tenantId ?? null,
1203
+ organizationScope: search.organizationScope,
1204
+ })
1205
+ if (applied) return q
1206
+ } else {
1207
+ this.logSearchDebug('search:index-doc-skip-empty-hashes', {
1208
+ entity: entityType,
1209
+ field: key,
1210
+ value,
1211
+ })
1212
+ }
1213
+ return q
1214
+ }
1215
+ switch (op) {
1216
+ case 'eq':
1217
+ return q.where(text, '=', value as Knex.Value)
1218
+ case 'ne':
1219
+ return q.where(text, '!=', value as Knex.Value)
1220
+ case 'in':
1221
+ return q.whereIn(text as any, this.toArray(value) as readonly Knex.Value[])
1222
+ case 'nin':
1223
+ return q.whereNotIn(text as any, this.toArray(value) as readonly Knex.Value[])
1224
+ case 'like':
1225
+ return q.where(text, 'like', value as Knex.Value)
1226
+ case 'ilike':
1227
+ return q.where(text, 'ilike', value as Knex.Value)
1228
+ case 'exists':
1229
+ return value
1230
+ ? q.whereRaw(`${text.toString()} is not null`)
1231
+ : q.whereRaw(`${text.toString()} is null`)
1232
+ case 'gt':
1233
+ case 'gte':
1234
+ case 'lt':
1235
+ case 'lte': {
1236
+ const operator = op === 'gt' ? '>' : op === 'gte' ? '>=' : op === 'lt' ? '<' : '<='
1237
+ return q.where(text, operator, value as Knex.Value)
1238
+ }
1239
+ default:
1240
+ return q
1241
+ }
1242
+ }
1243
+
1144
1244
  private async queryCustomEntity<T = unknown>(entity: string, opts: QueryOptions = {}): Promise<QueryResult<T>> {
1145
1245
  const knex = this.getKnex()
1146
1246
  const alias = 'ce'
@@ -4,6 +4,7 @@ import { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encrypt
4
4
  import { decryptIndexDocForSearch, encryptIndexDocForStorage } from '@open-mercato/shared/lib/encryption/indexDoc'
5
5
  import type { Knex } from 'knex'
6
6
  import { replaceSearchTokensForRecord, deleteSearchTokensForRecord } from './search-tokens'
7
+ import { attachAggregateSearchField } from './document'
7
8
 
8
9
  type BuildDocParams = {
9
10
  entityType: string // '<module>:<entity>'
@@ -91,6 +92,7 @@ export async function buildIndexDoc(em: EntityManager, params: BuildDocParams):
91
92
  } catch {}
92
93
 
93
94
  try {
95
+ doc = attachAggregateSearchField(doc)
94
96
  const encryption = resolveTenantEncryptionService(em as any)
95
97
  doc = await encryptIndexDocForStorage(
96
98
  params.entityType,
@@ -8,13 +8,12 @@ import { E } from '#generated/entities.ids.generated'
8
8
  import * as F from '#generated/entities/dictionary_entry'
9
9
  import { statusDictionaryCreateSchema, statusDictionaryUpdateSchema } from '../../data/validators'
10
10
  import { getSalesDictionaryDefinition, ensureSalesDictionary, type SalesDictionaryKind } from '../../lib/dictionaries'
11
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
11
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'
12
12
  import {
13
13
  createPagedListResponseSchema,
14
14
  createSalesCrudOpenApi,
15
15
  defaultDeleteRequestSchema,
16
16
  } from '../openapi'
17
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
18
17
 
19
18
  const rawBodySchema = z.object({}).passthrough()
20
19
 
@@ -143,13 +142,8 @@ const crud = makeCrudRoute({
143
142
  const filters: Record<string, unknown> = {
144
143
  dictionary_id: dictionaryId,
145
144
  }
146
- if (query.search && query.search.trim().length > 0) {
147
- const term = `%${escapeLikePattern(query.search.trim())}%`
148
- filters.$or = [
149
- { [F.value]: { $ilike: term } },
150
- { [F.label]: { $ilike: term } },
151
- ]
152
- }
145
+ const searchFilter = buildAggregateSearchFilter(query.search)
146
+ if (searchFilter) Object.assign(filters, searchFilter)
153
147
  return filters
154
148
  },
155
149
  transformItem: (item: any) => ({
@@ -5,7 +5,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { SalesChannel } from '../../data/entities'
7
7
  import { channelCreateSchema, channelUpdateSchema } from '../../data/validators'
8
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
8
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'
9
9
  import { E } from '#generated/entities.ids.generated'
10
10
  import * as F from '#generated/entities/sales_channel'
11
11
  import {
@@ -14,7 +14,6 @@ import {
14
14
  defaultDeleteRequestSchema,
15
15
  } from '../openapi'
16
16
  import { CatalogOffer } from '@open-mercato/core/modules/catalog/data/entities'
17
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
18
17
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
19
18
 
20
19
  const rawBodySchema = z.object({}).passthrough()
@@ -76,14 +75,8 @@ export function buildSearchFilters(query: z.infer<typeof listSchema>): Record<st
76
75
  const ids = parseIdList(query.ids)
77
76
  if (ids.length) filters.id = { $in: ids }
78
77
  }
79
- if (query.search && query.search.trim().length > 0) {
80
- const term = `%${escapeLikePattern(query.search.trim())}%`
81
- filters.$or = [
82
- { name: { $ilike: term } },
83
- { code: { $ilike: term } },
84
- { description: { $ilike: term } },
85
- ]
86
- }
78
+ const searchFilter = buildAggregateSearchFilter(query.search)
79
+ if (searchFilter) Object.assign(filters, searchFilter)
87
80
  const isActive = parseBooleanToken(query.isActive)
88
81
  if (isActive !== null) filters.is_active = isActive
89
82
  return filters
@@ -4,7 +4,7 @@ import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fi
4
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
5
  import { SalesDeliveryWindow } from '../../data/entities'
6
6
  import { deliveryWindowCreateSchema, deliveryWindowUpdateSchema } from '../../data/validators'
7
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
7
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'
8
8
  import { E } from '#generated/entities.ids.generated'
9
9
  import * as F from '#generated/entities/sales_delivery_window'
10
10
  import {
@@ -12,7 +12,6 @@ import {
12
12
  createSalesCrudOpenApi,
13
13
  defaultDeleteRequestSchema,
14
14
  } from '../openapi'
15
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
16
15
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
17
16
 
18
17
  const rawBodySchema = z.object({}).passthrough()
@@ -59,14 +58,8 @@ const deliveryWindowListResponseSchema = createPagedListResponseSchema(deliveryW
59
58
 
60
59
  function buildFilters(query: z.infer<typeof listSchema>): Record<string, unknown> {
61
60
  const filters: Record<string, unknown> = {}
62
- if (query.search && query.search.trim().length > 0) {
63
- const term = `%${escapeLikePattern(query.search.trim())}%`
64
- filters.$or = [
65
- { name: { $ilike: term } },
66
- { code: { $ilike: term } },
67
- { description: { $ilike: term } },
68
- ]
69
- }
61
+ const searchFilter = buildAggregateSearchFilter(query.search)
62
+ if (searchFilter) Object.assign(filters, searchFilter)
70
63
  const isActive = parseBooleanToken(query.isActive)
71
64
  if (isActive !== null) filters.is_active = isActive
72
65
  return filters
@@ -4,7 +4,7 @@ import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fi
4
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
5
  import { SalesPaymentMethod } from '../../data/entities'
6
6
  import { paymentMethodCreateSchema, paymentMethodUpdateSchema } from '../../data/validators'
7
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
7
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'
8
8
  import { E } from '#generated/entities.ids.generated'
9
9
  import * as F from '#generated/entities/sales_payment_method'
10
10
  import {
@@ -12,7 +12,6 @@ import {
12
12
  createSalesCrudOpenApi,
13
13
  defaultDeleteRequestSchema,
14
14
  } from '../openapi'
15
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
16
15
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
17
16
 
18
17
  const rawBodySchema = z.object({}).passthrough()
@@ -59,15 +58,8 @@ const paymentMethodListResponseSchema = createPagedListResponseSchema(paymentMet
59
58
 
60
59
  function buildFilters(query: z.infer<typeof listSchema>): Record<string, unknown> {
61
60
  const filters: Record<string, unknown> = {}
62
- if (query.search && query.search.trim().length > 0) {
63
- const term = `%${escapeLikePattern(query.search.trim())}%`
64
- filters.$or = [
65
- { name: { $ilike: term } },
66
- { code: { $ilike: term } },
67
- { provider_key: { $ilike: term } },
68
- { description: { $ilike: term } },
69
- ]
70
- }
61
+ const searchFilter = buildAggregateSearchFilter(query.search)
62
+ if (searchFilter) Object.assign(filters, searchFilter)
71
63
  const isActive = parseBooleanToken(query.isActive)
72
64
  if (isActive !== null) filters.is_active = isActive
73
65
  return filters
@@ -5,7 +5,7 @@ import { sanitizeSearchTerm, parseBooleanFlag } from '@open-mercato/core/modules
5
5
  import { E } from '#generated/entities.ids.generated'
6
6
  import * as F from '#generated/entities/catalog_price_kind'
7
7
  import { createPagedListResponseSchema, createSalesCrudOpenApi } from '../openapi'
8
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
8
+ import { buildAggregateSearchFilter } from '../utils'
9
9
 
10
10
  const routeMetadata = {
11
11
  GET: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },
@@ -45,10 +45,8 @@ const crud = makeCrudRoute({
45
45
  buildFilters: async (query) => {
46
46
  const filters: Record<string, unknown> = {}
47
47
  const term = sanitizeSearchTerm(query.search)
48
- if (term) {
49
- const like = `%${escapeLikePattern(term)}%`
50
- filters.$or = [{ [F.code]: { $ilike: like } }, { [F.title]: { $ilike: like } }]
51
- }
48
+ const searchFilter = buildAggregateSearchFilter(term)
49
+ if (searchFilter) Object.assign(filters, searchFilter)
52
50
  const isActive = parseBooleanFlag(query.isActive)
53
51
  if (isActive !== undefined) {
54
52
  filters[F.is_active] = isActive
@@ -4,7 +4,7 @@ import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fi
4
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
5
  import { SalesShippingMethod } from '../../data/entities'
6
6
  import { shippingMethodCreateSchema, shippingMethodUpdateSchema } from '../../data/validators'
7
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
7
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'
8
8
  import { E } from '#generated/entities.ids.generated'
9
9
  import * as F from '#generated/entities/sales_shipping_method'
10
10
  import {
@@ -12,7 +12,6 @@ import {
12
12
  createSalesCrudOpenApi,
13
13
  defaultDeleteRequestSchema,
14
14
  } from '../openapi'
15
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
16
15
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
17
16
 
18
17
  const rawBodySchema = z.object({}).passthrough()
@@ -65,15 +64,8 @@ const shippingMethodListResponseSchema = createPagedListResponseSchema(shippingM
65
64
 
66
65
  function buildFilters(query: z.infer<typeof listSchema>): Record<string, unknown> {
67
66
  const filters: Record<string, unknown> = {}
68
- if (query.search && query.search.trim().length > 0) {
69
- const term = `%${escapeLikePattern(query.search.trim())}%`
70
- filters.$or = [
71
- { name: { $ilike: term } },
72
- { code: { $ilike: term } },
73
- { carrier_code: { $ilike: term } },
74
- { service_level: { $ilike: term } },
75
- ]
76
- }
67
+ const searchFilter = buildAggregateSearchFilter(query.search)
68
+ if (searchFilter) Object.assign(filters, searchFilter)
77
69
  if (query.currency && query.currency.trim().length > 0) {
78
70
  filters.currency_code = query.currency.trim().toUpperCase()
79
71
  }
@@ -4,10 +4,9 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
4
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
5
  import { SalesDocumentTag } from '../../data/entities'
6
6
  import { salesTagCreateSchema, salesTagUpdateSchema } from '../../data/validators'
7
- import { withScopedPayload } from '../utils'
7
+ import { buildAggregateSearchFilter, withScopedPayload } from '../utils'
8
8
  import { createPagedListResponseSchema, createSalesCrudOpenApi, defaultOkResponseSchema } from '../openapi'
9
9
  import { slugifyTagLabel } from '@open-mercato/shared/lib/utils'
10
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
11
10
 
12
11
  const rawBodySchema = z.object({}).passthrough()
13
12
 
@@ -44,13 +43,8 @@ const crud = makeCrudRoute({
44
43
  fields: ['id', 'slug', 'label', 'color', 'description', 'organization_id', 'tenant_id'],
45
44
  buildFilters: async (query: any) => {
46
45
  const filters: Record<string, any> = {}
47
- if (query.search) {
48
- const pattern = `%${escapeLikePattern(query.search)}%`
49
- filters.$or = [
50
- { label: { $ilike: pattern } },
51
- { slug: { $ilike: pattern } },
52
- ]
53
- }
46
+ const searchFilter = buildAggregateSearchFilter(query.search)
47
+ if (searchFilter) Object.assign(filters, searchFilter)
54
48
  return filters
55
49
  },
56
50
  },
@@ -5,10 +5,9 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
5
5
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
6
6
  import { SalesTaxRate } from '../../data/entities'
7
7
  import { taxRateCreateSchema, taxRateUpdateSchema } from '../../data/validators'
8
- import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
8
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'
9
9
  import { E } from '#generated/entities.ids.generated'
10
10
  import * as F from '#generated/entities/sales_tax_rate'
11
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
12
11
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
13
12
 
14
13
  const rawBodySchema = z.object({}).passthrough()
@@ -76,17 +75,8 @@ const taxRateDeleteSchema = z.object({
76
75
 
77
76
  function buildFilters(query: z.infer<typeof listSchema>): Record<string, unknown> {
78
77
  const filters: Record<string, unknown> = {}
79
- if (query.search && query.search.trim().length > 0) {
80
- const term = `%${escapeLikePattern(query.search.trim())}%`
81
- filters.$or = [
82
- { name: { $ilike: term } },
83
- { code: { $ilike: term } },
84
- { country_code: { $ilike: term } },
85
- { region_code: { $ilike: term } },
86
- { postal_code: { $ilike: term } },
87
- { city: { $ilike: term } },
88
- ]
89
- }
78
+ const searchFilter = buildAggregateSearchFilter(query.search)
79
+ if (searchFilter) Object.assign(filters, searchFilter)
90
80
  if (query.country && query.country.trim().length > 0) {
91
81
  filters.country_code = query.country.trim().toUpperCase()
92
82
  }
@@ -1,4 +1,5 @@
1
1
  import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'
2
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
2
3
 
3
4
  const {
4
5
  withScopedPayload,
@@ -14,3 +15,11 @@ const {
14
15
  })
15
16
 
16
17
  export { withScopedPayload, parseScopedCommandInput, requireRecordId, resolveCrudRecordId }
18
+
19
+ export function buildAggregateSearchFilter(search?: string | null): Record<string, unknown> | null {
20
+ const term = typeof search === 'string' ? search.trim() : ''
21
+ if (!term) return null
22
+ return {
23
+ search_text: { $ilike: `%${escapeLikePattern(term)}%` },
24
+ }
25
+ }
@@ -6,13 +6,12 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
6
  import { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'
7
7
  import { statusDictionaryCreateSchema, statusDictionaryUpdateSchema } from '../data/validators'
8
8
  import { getSalesDictionaryDefinition, ensureSalesDictionary, type SalesDictionaryKind } from './dictionaries'
9
- import { parseScopedCommandInput, resolveCrudRecordId } from '../api/utils'
9
+ import { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../api/utils'
10
10
  import {
11
11
  createPagedListResponseSchema,
12
12
  createSalesCrudOpenApi,
13
13
  defaultDeleteRequestSchema,
14
14
  } from '../api/openapi'
15
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
16
15
 
17
16
  interface StatusDictionaryRouteConfig {
18
17
  kind: SalesDictionaryKind
@@ -153,13 +152,8 @@ export function makeStatusDictionaryRoute(config: StatusDictionaryRouteConfig) {
153
152
  const filters: Record<string, unknown> = {
154
153
  dictionary_id: dictionaryId,
155
154
  }
156
- if (query.search && query.search.trim().length > 0) {
157
- const term = `%${escapeLikePattern(query.search.trim())}%`
158
- filters.$or = [
159
- { [F.value]: { $ilike: term } },
160
- { [F.label]: { $ilike: term } },
161
- ]
162
- }
155
+ const searchFilter = buildAggregateSearchFilter(query.search)
156
+ if (searchFilter) Object.assign(filters, searchFilter)
163
157
  return filters
164
158
  },
165
159
  transformItem: (item: Record<string, unknown>) => ({
@@ -17,6 +17,7 @@ import {
17
17
  updateWorkflowDefinitionInputSchema,
18
18
  type UpdateWorkflowDefinitionApiInput,
19
19
  } from '../../../data/validators'
20
+ import { serializeWorkflowDefinition } from '../serialize'
20
21
 
21
22
  export const metadata = {
22
23
  requireAuth: true,
@@ -66,7 +67,7 @@ export async function GET(
66
67
  )
67
68
  }
68
69
 
69
- return NextResponse.json({ data: definition })
70
+ return NextResponse.json({ data: serializeWorkflowDefinition(definition) })
70
71
  } catch (error) {
71
72
  console.error('Error getting workflow definition:', error)
72
73
  return NextResponse.json(
@@ -162,7 +163,7 @@ export async function PUT(
162
163
  await em.flush()
163
164
 
164
165
  return NextResponse.json({
165
- data: definition,
166
+ data: serializeWorkflowDefinition(definition),
166
167
  message: 'Workflow definition updated successfully',
167
168
  })
168
169
  } catch (error) {