@open-mercato/core 0.8.1-develop.7215.1.60786cf836 → 0.8.1-develop.7218.1.9f22882799

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 (27) hide show
  1. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +1 -4
  2. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
  3. package/dist/modules/customers/components/calendar/CalendarToolbar.js +3 -0
  4. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
  5. package/dist/modules/customers/components/calendar/editor/RepeatField.js +1 -3
  6. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  7. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +1 -5
  8. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  9. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  10. package/dist/modules/entities/lib/helpers.js +2 -2
  11. package/dist/modules/entities/lib/helpers.js.map +2 -2
  12. package/dist/modules/query_index/lib/batch.js +2 -2
  13. package/dist/modules/query_index/lib/batch.js.map +2 -2
  14. package/dist/modules/query_index/lib/document.js +7 -1
  15. package/dist/modules/query_index/lib/document.js.map +2 -2
  16. package/dist/modules/query_index/lib/indexer.js +9 -2
  17. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  18. package/package.json +7 -7
  19. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +1 -4
  20. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +3 -0
  21. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +0 -3
  22. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +1 -7
  23. package/src/modules/customers/components/calendar/editor/inputs.tsx +0 -1
  24. package/src/modules/entities/lib/helpers.ts +2 -2
  25. package/src/modules/query_index/lib/batch.ts +8 -2
  26. package/src/modules/query_index/lib/document.ts +28 -0
  27. package/src/modules/query_index/lib/indexer.ts +15 -2
@@ -19,7 +19,6 @@ function DateTimeRow({
19
19
  date,
20
20
  time,
21
21
  showTime,
22
- locale,
23
22
  onDateChange,
24
23
  onTimeChange,
25
24
  }: {
@@ -27,7 +26,6 @@ function DateTimeRow({
27
26
  date: string
28
27
  time: string
29
28
  showTime: boolean
30
- locale: string
31
29
  onDateChange(next: string): void
32
30
  onTimeChange(next: string): void
33
31
  }) {
@@ -35,7 +33,7 @@ function DateTimeRow({
35
33
  <div className="flex w-full items-end gap-2.5">
36
34
  <div className="flex min-w-0 flex-1 flex-col gap-1.5">
37
35
  <span className={LABEL_CLASS}>{label}</span>
38
- <DateControl value={date} onChange={onDateChange} ariaLabel={label} locale={locale} />
36
+ <DateControl value={date} onChange={onDateChange} ariaLabel={label} />
39
37
  </div>
40
38
  {showTime ? <TimeControl value={time} onChange={onTimeChange} ariaLabel={label} /> : null}
41
39
  </div>
@@ -51,7 +49,6 @@ export function ScheduleSection({
51
49
  startTime,
52
50
  endDate,
53
51
  endTime,
54
- locale,
55
52
  endsError,
56
53
  onAllDayChange,
57
54
  onDateChange,
@@ -67,7 +64,6 @@ export function ScheduleSection({
67
64
  startTime: string
68
65
  endDate: string
69
66
  endTime: string
70
- locale: string
71
67
  endsError?: string | null
72
68
  onAllDayChange(next: boolean): void
73
69
  onDateChange(next: string): void
@@ -91,7 +87,6 @@ export function ScheduleSection({
91
87
  date={date}
92
88
  time={startTime}
93
89
  showTime={showTime}
94
- locale={locale}
95
90
  onDateChange={onDateChange}
96
91
  onTimeChange={onStartTimeChange}
97
92
  />
@@ -101,7 +96,6 @@ export function ScheduleSection({
101
96
  date={endDate}
102
97
  time={endTime}
103
98
  showTime={showTime}
104
- locale={locale}
105
99
  onDateChange={onEndDateChange}
106
100
  onTimeChange={onEndTimeChange}
107
101
  />
@@ -116,7 +116,6 @@ export function DateControl({
116
116
  value: string
117
117
  onChange(next: string): void
118
118
  ariaLabel: string
119
- locale: string
120
119
  className?: string
121
120
  }) {
122
121
  const [open, setOpen] = React.useState(false)
@@ -135,7 +135,7 @@ export async function setRecordCustomFields(
135
135
  const cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: new Date() })
136
136
  clearValueColumns(cf)
137
137
  const stored = encrypted
138
- ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache)
138
+ ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey })
139
139
  : val
140
140
  switch (col) {
141
141
  case 'valueText': cf.valueText = stored == null ? null : String(stored); break
@@ -152,7 +152,7 @@ export async function setRecordCustomFields(
152
152
 
153
153
  const column: keyof CustomFieldValue = encrypted ? 'valueText' : def ? columnFromKind(def.kind) : columnFromJsValue(raw as Primitive)
154
154
  const storedValue = encrypted
155
- ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache)
155
+ ? await encryptCustomFieldValue(raw as Primitive, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey })
156
156
  : raw
157
157
 
158
158
  let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey })
@@ -1,7 +1,7 @@
1
1
  import { type Kysely, sql } from 'kysely'
2
2
  import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
3
3
  import { isUniqueViolation } from '@open-mercato/shared/lib/db/pg-errors'
4
- import { buildIndexDocument, type IndexCustomFieldValue } from './document'
4
+ import { buildIndexDocument, rebuildAggregateSearchField, type IndexCustomFieldValue } from './document'
5
5
  import { replaceSearchTokensForBatch, isSearchDebugEnabled } from './search-tokens'
6
6
  import { createLogger } from '@open-mercato/shared/lib/logger'
7
7
  import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
@@ -304,7 +304,13 @@ export async function upsertIndexBatch(
304
304
  tenantId: scopeTenant ?? null,
305
305
  })
306
306
  if (decrypted && typeof decrypted === 'object') {
307
- tokenDoc = decrypted
307
+ // Rebuilt on the decrypted copy: `search_text` is on no encryption map, so the
308
+ // aggregate composed at document-build time still concatenates the ciphertext
309
+ // values the row carries at rest and tokenizes into rows nothing matches (#5625).
310
+ // The copy keeps `doc` — the encrypted row written to `entity_indexes` — untouched.
311
+ // A caller that passes no `decryptDoc` keeps the build-time aggregate by design:
312
+ // without a decryption step there is no plaintext to recompose it from.
313
+ tokenDoc = rebuildAggregateSearchField(decrypted, { entityType, config: searchConfig })
308
314
  }
309
315
  } catch (decryptError) {
310
316
  // Only affects the search tokens built below; the indexed document itself is
@@ -109,6 +109,34 @@ export function attachAggregateSearchField(
109
109
  return doc
110
110
  }
111
111
 
112
+ /**
113
+ * Recomputes the aggregate on a shallow copy of `doc`.
114
+ *
115
+ * Search tokens are built from the *decrypted* index document, while the document
116
+ * persisted in `entity_indexes` stays encrypted at rest. The aggregate composed during
117
+ * document build therefore concatenates ciphertext for every field an encryption map
118
+ * covers, and `decryptIndexDocForSearch` cannot repair it because `search_text` is on no
119
+ * map (#5625). Token writers call this on the decrypted document so the aggregate is
120
+ * tokenized from the same plaintext a user actually searches for; the copy keeps the
121
+ * caller's stored document untouched.
122
+ *
123
+ * A pre-existing aggregate is blanked before recomposition rather than deleted: an empty
124
+ * string yields no tokens, but it keeps the key on the document, and
125
+ * `replaceSearchTokensForRecord` scopes its DELETE to the document's own field names. A
126
+ * deleted key would drop `search_text` out of that scope and strand the ciphertext-derived
127
+ * rows with no writer left to remove them.
128
+ */
129
+ export function rebuildAggregateSearchField(
130
+ doc: Record<string, unknown>,
131
+ options: AggregateSearchOptions = {},
132
+ ): Record<string, unknown> {
133
+ const copy = { ...doc }
134
+ // `collectAggregateSearchValues` skips the aggregate's own key, so this blank is
135
+ // overwritten whenever anything survives the blocklist and stays blank otherwise.
136
+ if (AGGREGATE_SEARCH_FIELD in copy) copy[AGGREGATE_SEARCH_FIELD] = ''
137
+ return attachAggregateSearchField(copy, options)
138
+ }
139
+
112
140
  export function buildIndexDocument(
113
141
  baseRow: Record<string, unknown>,
114
142
  customFieldValues: Iterable<IndexCustomFieldValue> = [],
@@ -10,8 +10,9 @@ import {
10
10
  } from '@open-mercato/shared/lib/crud/custom-field-definition-index'
11
11
  import { type Kysely, type Transaction, sql } from 'kysely'
12
12
  import { createLogger } from '@open-mercato/shared/lib/logger'
13
+ import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
13
14
  import { replaceSearchTokensForRecord, deleteSearchTokensForRecord } from './search-tokens'
14
- import { attachAggregateSearchField } from './document'
15
+ import { attachAggregateSearchField, rebuildAggregateSearchField } from './document'
15
16
 
16
17
  const logger = createLogger('query_index').child({ component: 'indexer' })
17
18
 
@@ -400,12 +401,24 @@ export async function reindexSearchTokensForRecord(
400
401
  dekKeyCache,
401
402
  )
402
403
  })()
404
+ // Rebuilt on the decrypted document: the aggregate stored in `entity_indexes` was
405
+ // composed from the row as it sits at rest, so for every encryption-mapped field it
406
+ // holds ciphertext no user query can ever match (#5625). `resolveSearchConfig` re-parses
407
+ // the environment on every call, so it is resolved once and threaded into both the
408
+ // rebuild and the token writer — that also makes the aggregate's blocklist filter and
409
+ // the per-field one provably read the same snapshot, as the batch path already does.
410
+ const searchConfig = resolveSearchConfig()
411
+ const searchDoc = rebuildAggregateSearchField(await tokenDoc, {
412
+ entityType: args.entityType,
413
+ config: searchConfig,
414
+ })
403
415
  await replaceSearchTokensForRecord(db, {
404
416
  entityType: args.entityType,
405
417
  recordId: args.recordId,
406
418
  organizationId: args.organizationId ?? null,
407
419
  tenantId: args.tenantId ?? null,
408
- doc: await tokenDoc,
420
+ doc: searchDoc,
421
+ config: searchConfig,
409
422
  }, { trx: args.trx })
410
423
  }
411
424