@open-mercato/core 0.8.1-develop.7215.1.60786cf836 → 0.8.1-develop.7219.1.f96cb57e70
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.
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +1 -4
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +3 -0
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +1 -3
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +1 -5
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/entities/lib/helpers.js +2 -2
- package/dist/modules/entities/lib/helpers.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +2 -2
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/document.js +7 -1
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +9 -2
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +1 -4
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +0 -3
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +1 -7
- package/src/modules/customers/components/calendar/editor/inputs.tsx +0 -1
- package/src/modules/entities/lib/helpers.ts +2 -2
- package/src/modules/query_index/lib/batch.ts +8 -2
- package/src/modules/query_index/lib/document.ts +28 -0
- 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}
|
|
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
|
/>
|
|
@@ -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
|
-
|
|
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:
|
|
420
|
+
doc: searchDoc,
|
|
421
|
+
config: searchConfig,
|
|
409
422
|
}, { trx: args.trx })
|
|
410
423
|
}
|
|
411
424
|
|