@open-mercato/core 0.8.1-develop.7207.1.901ef7e47a → 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 (62) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +6 -2
  3. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  4. package/dist/modules/catalog/backend/catalog/products/create/page.js +7 -3
  5. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  6. package/dist/modules/catalog/components/products/ProductUomSection.js +11 -22
  7. package/dist/modules/catalog/components/products/ProductUomSection.js.map +2 -2
  8. package/dist/modules/catalog/components/products/productForm.js +26 -1
  9. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  10. package/dist/modules/configs/lib/upgrade-actions.js +1 -0
  11. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  12. package/dist/modules/configs/services/upgradeActionsService.js +9 -2
  13. package/dist/modules/configs/services/upgradeActionsService.js.map +2 -2
  14. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +1 -4
  15. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
  16. package/dist/modules/customers/components/calendar/CalendarToolbar.js +3 -0
  17. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
  18. package/dist/modules/customers/components/calendar/editor/RepeatField.js +1 -3
  19. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  20. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +1 -5
  21. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  22. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  23. package/dist/modules/entities/lib/definition-scope-where.js +18 -0
  24. package/dist/modules/entities/lib/definition-scope-where.js.map +7 -0
  25. package/dist/modules/entities/lib/definition-scope.js +2 -8
  26. package/dist/modules/entities/lib/definition-scope.js.map +2 -2
  27. package/dist/modules/entities/lib/helpers.js +6 -32
  28. package/dist/modules/entities/lib/helpers.js.map +2 -2
  29. package/dist/modules/entities/lib/scoped-field-defs.js +5 -10
  30. package/dist/modules/entities/lib/scoped-field-defs.js.map +2 -2
  31. package/dist/modules/query_index/lib/batch.js +2 -2
  32. package/dist/modules/query_index/lib/batch.js.map +2 -2
  33. package/dist/modules/query_index/lib/document.js +7 -1
  34. package/dist/modules/query_index/lib/document.js.map +2 -2
  35. package/dist/modules/query_index/lib/engine.js.map +1 -1
  36. package/dist/modules/query_index/lib/indexer.js +9 -2
  37. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  38. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  39. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js +15 -1
  40. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js.map +2 -2
  41. package/package.json +7 -7
  42. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +6 -2
  43. package/src/modules/catalog/backend/catalog/products/create/page.tsx +7 -6
  44. package/src/modules/catalog/components/products/ProductUomSection.tsx +11 -31
  45. package/src/modules/catalog/components/products/productForm.ts +53 -0
  46. package/src/modules/configs/lib/upgrade-actions.ts +7 -0
  47. package/src/modules/configs/services/upgradeActionsService.ts +17 -2
  48. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +1 -4
  49. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +3 -0
  50. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +0 -3
  51. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +1 -7
  52. package/src/modules/customers/components/calendar/editor/inputs.tsx +0 -1
  53. package/src/modules/entities/lib/definition-scope-where.ts +27 -0
  54. package/src/modules/entities/lib/definition-scope.ts +2 -10
  55. package/src/modules/entities/lib/helpers.ts +6 -35
  56. package/src/modules/entities/lib/scoped-field-defs.ts +6 -15
  57. package/src/modules/query_index/lib/batch.ts +8 -2
  58. package/src/modules/query_index/lib/document.ts +28 -0
  59. package/src/modules/query_index/lib/engine.ts +4 -4
  60. package/src/modules/query_index/lib/indexer.ts +15 -2
  61. package/src/modules/query_index/lib/search-tokens.ts +31 -0
  62. package/src/modules/wms/components/backend/AdjustInventoryDialog.tsx +21 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/components/calendar/editor/ScheduleSection.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { EditorDateLabel } from '../../../lib/calendar/editorPayload'\nimport { multiDayEventSpan } from '../../../lib/calendar/labels'\nimport { AllDayToggle, DateControl, LABEL_CLASS, TimeControl } from './inputs'\n\nconst DATE_LABEL_TEXT: Record<EditorDateLabel, { key: string; fallback: string }> = {\n starts: { key: 'customers.calendar.editor.dates.starts', fallback: 'Starts' },\n when: { key: 'customers.calendar.editor.dates.when', fallback: 'When' },\n sent: { key: 'customers.calendar.editor.dates.sent', fallback: 'Sent' },\n logged: { key: 'customers.calendar.editor.dates.logged', fallback: 'Logged' },\n due: { key: 'customers.calendar.editor.dates.due', fallback: 'Due' },\n}\n\nfunction DateTimeRow({\n label,\n date,\n time,\n showTime,\n locale,\n onDateChange,\n onTimeChange,\n}: {\n label: string\n date: string\n time: string\n showTime: boolean\n locale: string\n onDateChange(next: string): void\n onTimeChange(next: string): void\n}) {\n return (\n <div className=\"flex w-full items-end gap-2.5\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <span className={LABEL_CLASS}>{label}</span>\n <DateControl value={date} onChange={onDateChange} ariaLabel={label} locale={locale} />\n </div>\n {showTime ? <TimeControl value={time} onChange={onTimeChange} ariaLabel={label} /> : null}\n </div>\n )\n}\n\nexport function ScheduleSection({\n dateLabel,\n hasAllDay,\n hasEnd,\n allDay,\n date,\n startTime,\n endDate,\n endTime,\n locale,\n endsError,\n onAllDayChange,\n onDateChange,\n onStartTimeChange,\n onEndDateChange,\n onEndTimeChange,\n}: {\n dateLabel: EditorDateLabel\n hasAllDay: boolean\n hasEnd: boolean\n allDay: boolean\n date: string\n startTime: string\n endDate: string\n endTime: string\n locale: string\n endsError?: string | null\n onAllDayChange(next: boolean): void\n onDateChange(next: string): void\n onStartTimeChange(next: string): void\n onEndDateChange(next: string): void\n onEndTimeChange(next: string): void\n}) {\n const t = useT()\n const showTime = !(hasAllDay && allDay)\n const multiDaySpan = hasEnd && !endsError ? multiDayEventSpan(date, endDate) : 0\n return (\n <div className=\"flex w-full flex-col gap-2.5\">\n {hasAllDay ? (\n <div className=\"flex w-full items-center justify-between\">\n <span className={LABEL_CLASS}>{t('customers.calendar.editor.allDay', 'All day')}</span>\n <AllDayToggle checked={allDay} onCheckedChange={onAllDayChange} label={t('customers.calendar.editor.allDay', 'All day')} />\n </div>\n ) : null}\n <DateTimeRow\n label={t(DATE_LABEL_TEXT[dateLabel].key, DATE_LABEL_TEXT[dateLabel].fallback)}\n date={date}\n time={startTime}\n showTime={showTime}\n locale={locale}\n onDateChange={onDateChange}\n onTimeChange={onStartTimeChange}\n />\n {hasEnd ? (\n <DateTimeRow\n label={t('customers.calendar.editor.dates.ends', 'Ends')}\n date={endDate}\n time={endTime}\n showTime={showTime}\n locale={locale}\n onDateChange={onEndDateChange}\n onTimeChange={onEndTimeChange}\n />\n ) : null}\n {endsError ? <p className=\"text-xs text-status-error-text\">{endsError}</p> : null}\n {multiDaySpan > 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.calendar.editor.multiDayHint', 'Multi-day event \u00B7 {count} days', { count: multiDaySpan })}\n </p>\n ) : null}\n </div>\n )\n}\n"],
5
- "mappings": ";AAmCM,SACE,KADF;AAhCN,SAAS,YAAY;AAErB,SAAS,yBAAyB;AAClC,SAAS,cAAc,aAAa,aAAa,mBAAmB;AAEpE,MAAM,kBAA8E;AAAA,EAClF,QAAQ,EAAE,KAAK,0CAA0C,UAAU,SAAS;AAAA,EAC5E,MAAM,EAAE,KAAK,wCAAwC,UAAU,OAAO;AAAA,EACtE,MAAM,EAAE,KAAK,wCAAwC,UAAU,OAAO;AAAA,EACtE,QAAQ,EAAE,KAAK,0CAA0C,UAAU,SAAS;AAAA,EAC5E,KAAK,EAAE,KAAK,uCAAuC,UAAU,MAAM;AACrE;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE,qBAAC,SAAI,WAAU,iCACb;AAAA,yBAAC,SAAI,WAAU,wCACb;AAAA,0BAAC,UAAK,WAAW,aAAc,iBAAM;AAAA,MACrC,oBAAC,eAAY,OAAO,MAAM,UAAU,cAAc,WAAW,OAAO,QAAgB;AAAA,OACtF;AAAA,IACC,WAAW,oBAAC,eAAY,OAAO,MAAM,UAAU,cAAc,WAAW,OAAO,IAAK;AAAA,KACvF;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAgBG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,EAAE,aAAa;AAChC,QAAM,eAAe,UAAU,CAAC,YAAY,kBAAkB,MAAM,OAAO,IAAI;AAC/E,SACE,qBAAC,SAAI,WAAU,gCACZ;AAAA,gBACC,qBAAC,SAAI,WAAU,4CACb;AAAA,0BAAC,UAAK,WAAW,aAAc,YAAE,oCAAoC,SAAS,GAAE;AAAA,MAChF,oBAAC,gBAAa,SAAS,QAAQ,iBAAiB,gBAAgB,OAAO,EAAE,oCAAoC,SAAS,GAAG;AAAA,OAC3H,IACE;AAAA,IACJ;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,gBAAgB,SAAS,EAAE,KAAK,gBAAgB,SAAS,EAAE,QAAQ;AAAA,QAC5E;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA,cAAc;AAAA;AAAA,IAChB;AAAA,IACC,SACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,wCAAwC,MAAM;AAAA,QACvD,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd,cAAc;AAAA;AAAA,IAChB,IACE;AAAA,IACH,YAAY,oBAAC,OAAE,WAAU,kCAAkC,qBAAU,IAAO;AAAA,IAC5E,eAAe,IACd,oBAAC,OAAE,WAAU,iCACV,YAAE,0CAA0C,qCAAkC,EAAE,OAAO,aAAa,CAAC,GACxG,IACE;AAAA,KACN;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { EditorDateLabel } from '../../../lib/calendar/editorPayload'\nimport { multiDayEventSpan } from '../../../lib/calendar/labels'\nimport { AllDayToggle, DateControl, LABEL_CLASS, TimeControl } from './inputs'\n\nconst DATE_LABEL_TEXT: Record<EditorDateLabel, { key: string; fallback: string }> = {\n starts: { key: 'customers.calendar.editor.dates.starts', fallback: 'Starts' },\n when: { key: 'customers.calendar.editor.dates.when', fallback: 'When' },\n sent: { key: 'customers.calendar.editor.dates.sent', fallback: 'Sent' },\n logged: { key: 'customers.calendar.editor.dates.logged', fallback: 'Logged' },\n due: { key: 'customers.calendar.editor.dates.due', fallback: 'Due' },\n}\n\nfunction DateTimeRow({\n label,\n date,\n time,\n showTime,\n onDateChange,\n onTimeChange,\n}: {\n label: string\n date: string\n time: string\n showTime: boolean\n onDateChange(next: string): void\n onTimeChange(next: string): void\n}) {\n return (\n <div className=\"flex w-full items-end gap-2.5\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <span className={LABEL_CLASS}>{label}</span>\n <DateControl value={date} onChange={onDateChange} ariaLabel={label} />\n </div>\n {showTime ? <TimeControl value={time} onChange={onTimeChange} ariaLabel={label} /> : null}\n </div>\n )\n}\n\nexport function ScheduleSection({\n dateLabel,\n hasAllDay,\n hasEnd,\n allDay,\n date,\n startTime,\n endDate,\n endTime,\n endsError,\n onAllDayChange,\n onDateChange,\n onStartTimeChange,\n onEndDateChange,\n onEndTimeChange,\n}: {\n dateLabel: EditorDateLabel\n hasAllDay: boolean\n hasEnd: boolean\n allDay: boolean\n date: string\n startTime: string\n endDate: string\n endTime: string\n endsError?: string | null\n onAllDayChange(next: boolean): void\n onDateChange(next: string): void\n onStartTimeChange(next: string): void\n onEndDateChange(next: string): void\n onEndTimeChange(next: string): void\n}) {\n const t = useT()\n const showTime = !(hasAllDay && allDay)\n const multiDaySpan = hasEnd && !endsError ? multiDayEventSpan(date, endDate) : 0\n return (\n <div className=\"flex w-full flex-col gap-2.5\">\n {hasAllDay ? (\n <div className=\"flex w-full items-center justify-between\">\n <span className={LABEL_CLASS}>{t('customers.calendar.editor.allDay', 'All day')}</span>\n <AllDayToggle checked={allDay} onCheckedChange={onAllDayChange} label={t('customers.calendar.editor.allDay', 'All day')} />\n </div>\n ) : null}\n <DateTimeRow\n label={t(DATE_LABEL_TEXT[dateLabel].key, DATE_LABEL_TEXT[dateLabel].fallback)}\n date={date}\n time={startTime}\n showTime={showTime}\n onDateChange={onDateChange}\n onTimeChange={onStartTimeChange}\n />\n {hasEnd ? (\n <DateTimeRow\n label={t('customers.calendar.editor.dates.ends', 'Ends')}\n date={endDate}\n time={endTime}\n showTime={showTime}\n onDateChange={onEndDateChange}\n onTimeChange={onEndTimeChange}\n />\n ) : null}\n {endsError ? <p className=\"text-xs text-status-error-text\">{endsError}</p> : null}\n {multiDaySpan > 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('customers.calendar.editor.multiDayHint', 'Multi-day event \u00B7 {count} days', { count: multiDaySpan })}\n </p>\n ) : null}\n </div>\n )\n}\n"],
5
+ "mappings": ";AAiCM,SACE,KADF;AA9BN,SAAS,YAAY;AAErB,SAAS,yBAAyB;AAClC,SAAS,cAAc,aAAa,aAAa,mBAAmB;AAEpE,MAAM,kBAA8E;AAAA,EAClF,QAAQ,EAAE,KAAK,0CAA0C,UAAU,SAAS;AAAA,EAC5E,MAAM,EAAE,KAAK,wCAAwC,UAAU,OAAO;AAAA,EACtE,MAAM,EAAE,KAAK,wCAAwC,UAAU,OAAO;AAAA,EACtE,QAAQ,EAAE,KAAK,0CAA0C,UAAU,SAAS;AAAA,EAC5E,KAAK,EAAE,KAAK,uCAAuC,UAAU,MAAM;AACrE;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,SACE,qBAAC,SAAI,WAAU,iCACb;AAAA,yBAAC,SAAI,WAAU,wCACb;AAAA,0BAAC,UAAK,WAAW,aAAc,iBAAM;AAAA,MACrC,oBAAC,eAAY,OAAO,MAAM,UAAU,cAAc,WAAW,OAAO;AAAA,OACtE;AAAA,IACC,WAAW,oBAAC,eAAY,OAAO,MAAM,UAAU,cAAc,WAAW,OAAO,IAAK;AAAA,KACvF;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAeG;AACD,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,EAAE,aAAa;AAChC,QAAM,eAAe,UAAU,CAAC,YAAY,kBAAkB,MAAM,OAAO,IAAI;AAC/E,SACE,qBAAC,SAAI,WAAU,gCACZ;AAAA,gBACC,qBAAC,SAAI,WAAU,4CACb;AAAA,0BAAC,UAAK,WAAW,aAAc,YAAE,oCAAoC,SAAS,GAAE;AAAA,MAChF,oBAAC,gBAAa,SAAS,QAAQ,iBAAiB,gBAAgB,OAAO,EAAE,oCAAoC,SAAS,GAAG;AAAA,OAC3H,IACE;AAAA,IACJ;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,gBAAgB,SAAS,EAAE,KAAK,gBAAgB,SAAS,EAAE,QAAQ;AAAA,QAC5E;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,cAAc;AAAA;AAAA,IAChB;AAAA,IACC,SACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,wCAAwC,MAAM;AAAA,QACvD,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA,cAAc;AAAA,QACd,cAAc;AAAA;AAAA,IAChB,IACE;AAAA,IACH,YAAY,oBAAC,OAAE,WAAU,kCAAkC,qBAAU,IAAO;AAAA,IAC5E,eAAe,IACd,oBAAC,OAAE,WAAU,iCACV,YAAE,0CAA0C,qCAAkC,EAAE,OAAO,aAAa,CAAC,GACxG,IACE;AAAA,KACN;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/components/calendar/editor/inputs.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Plus } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { DatePicker } from '@open-mercato/ui/primitives/date-picker'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\n\nexport const CONTROL_BORDER = 'border border-input'\nexport const CONTROL_TEXT = 'text-sm text-foreground placeholder:text-muted-foreground'\nexport const LABEL_CLASS = 'text-xs font-medium text-muted-foreground'\nexport const DROPDOWN_PANEL_CLASS =\n 'absolute z-50 mt-1 max-h-56 w-full overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md'\n\n/**\n * Closes an editor dropdown on pointer-down outside the component or Escape.\n * Blur alone is unreliable inside the Radix dialog focus trap (#3552 feedback:\n * the Resources list stayed open), so dismissal listens on the document while\n * the dropdown is open. Returns the ref to attach to the component root.\n */\nexport function useDropdownDismiss(open: boolean, onClose: () => void): React.RefObject<HTMLDivElement | null> {\n const rootRef = React.useRef<HTMLDivElement | null>(null)\n React.useEffect(() => {\n if (!open) return\n const handlePointerDown = (event: PointerEvent) => {\n if (rootRef.current && event.target instanceof Node && !rootRef.current.contains(event.target)) onClose()\n }\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n // Swallow Escape so the dialog itself stays open.\n event.stopPropagation()\n onClose()\n }\n }\n document.addEventListener('pointerdown', handlePointerDown, true)\n document.addEventListener('keydown', handleKeyDown, true)\n return () => {\n document.removeEventListener('pointerdown', handlePointerDown, true)\n document.removeEventListener('keydown', handleKeyDown, true)\n }\n }, [open, onClose])\n return rootRef\n}\n\nexport function Field({ label, children, error, className }: { label: string; children: React.ReactNode; error?: string | null; className?: string }) {\n return (\n <div className={cn('flex w-full flex-col gap-1.5', className)}>\n <span className={LABEL_CLASS}>{label}</span>\n {children}\n {error ? <p className=\"text-xs text-status-error-text\">{error}</p> : null}\n </div>\n )\n}\n\nexport function UppercaseBadge({ style, className, children }: { style?: React.CSSProperties; className?: string; children: React.ReactNode }) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full bg-muted px-2 py-0.5 text-overline font-medium uppercase text-muted-foreground',\n className,\n )}\n style={style}\n >\n {children}\n </span>\n )\n}\n\nexport function AllDayToggle({ checked, onCheckedChange, label }: { checked: boolean; onCheckedChange(next: boolean): void; label: string }) {\n return (\n <Switch\n checked={checked}\n aria-label={label}\n onCheckedChange={onCheckedChange}\n className=\"h-6 w-10\"\n />\n )\n}\n\nfunction parseDateValue(value: string): Date | null {\n if (!value) return null\n const parsed = new Date(`${value}T00:00:00`)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n}\n\nfunction formatDateValue(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`\n}\n\n// Close the field's popover when the surrounding editor dialog scrolls, so a\n// portalled DS popover (DatePicker/Select) doesn't float over the form or drift\n// from its field (#3747 feedback). Radix ignores synthetic dismiss events, so\n// this drives the popover's controlled `open` state instead. The editor\n// dispatches `EDITOR_SCROLL_EVENT` on scroll.\nexport const EDITOR_SCROLL_EVENT = 'om-calendar-editor-scroll'\nfunction useCloseOnEditorScroll(setOpen: (open: boolean) => void) {\n React.useEffect(() => {\n const handler = () => setOpen(false)\n document.addEventListener(EDITOR_SCROLL_EVENT, handler)\n return () => document.removeEventListener(EDITOR_SCROLL_EVENT, handler)\n }, [setOpen])\n}\n\n// DS DatePicker (calendar popover) + DS Select (scrolling 30-min list) replace\n// the native `<input type=date|time>` overlays, which only opened via the\n// browser chevron and did not match the design system (#3747 feedback).\nexport function DateControl({\n value,\n onChange,\n ariaLabel,\n className,\n}: {\n value: string\n onChange(next: string): void\n ariaLabel: string\n locale: string\n className?: string\n}) {\n const [open, setOpen] = React.useState(false)\n useCloseOnEditorScroll(setOpen)\n return (\n <DatePicker\n value={parseDateValue(value)}\n onChange={(date) => { if (date) onChange(formatDateValue(date)) }}\n footer=\"none\"\n open={open}\n onOpenChange={setOpen}\n aria-label={ariaLabel}\n className={cn('w-full', className)}\n />\n )\n}\n\n// 24h times at 30-minute steps.\nconst TIME_OPTIONS: string[] = Array.from({ length: 48 }, (_, index) => {\n const hour = Math.floor(index / 2)\n const minute = index % 2 === 0 ? '00' : '30'\n return `${String(hour).padStart(2, '0')}:${minute}`\n})\n\nexport function TimeControl({ value, onChange, ariaLabel }: { value: string; onChange(next: string): void; ariaLabel: string }) {\n const [open, setOpen] = React.useState(false)\n useCloseOnEditorScroll(setOpen)\n // Keep an off-grid value (e.g. an imported 22:15) selectable.\n const options = value && !TIME_OPTIONS.includes(value) ? [value, ...TIME_OPTIONS] : TIME_OPTIONS\n return (\n <Select value={value} onValueChange={onChange} open={open} onOpenChange={setOpen}>\n <SelectTrigger aria-label={ariaLabel} className=\"h-9 w-32 shrink-0\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent className=\"max-h-64\">\n {options.map((time) => (\n <SelectItem key={time} value={time}>{time}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nexport function PersonChip({\n name,\n badge,\n compact,\n onRemove,\n removeLabel,\n}: {\n name: string\n badge?: React.ReactNode\n compact?: boolean\n onRemove?: () => void\n removeLabel?: string\n}) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center gap-1.5 rounded-full bg-muted pl-1 pr-2',\n compact ? 'py-0.5' : 'py-1',\n )}\n >\n <Avatar size=\"xs\" label={name} />\n <span className=\"max-w-40 truncate text-xs font-medium text-foreground\">{name}</span>\n {badge}\n {onRemove ? (\n <IconButton\n variant=\"ghost\"\n size=\"xs\"\n onClick={(event) => { event.stopPropagation(); onRemove() }}\n aria-label={removeLabel}\n className=\"size-5 shrink-0\"\n >\n <Plus aria-hidden className=\"size-3.5 rotate-45 opacity-50\" />\n </IconButton>\n ) : null}\n </span>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Plus } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { DatePicker } from '@open-mercato/ui/primitives/date-picker'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\n\nexport const CONTROL_BORDER = 'border border-input'\nexport const CONTROL_TEXT = 'text-sm text-foreground placeholder:text-muted-foreground'\nexport const LABEL_CLASS = 'text-xs font-medium text-muted-foreground'\nexport const DROPDOWN_PANEL_CLASS =\n 'absolute z-50 mt-1 max-h-56 w-full overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md'\n\n/**\n * Closes an editor dropdown on pointer-down outside the component or Escape.\n * Blur alone is unreliable inside the Radix dialog focus trap (#3552 feedback:\n * the Resources list stayed open), so dismissal listens on the document while\n * the dropdown is open. Returns the ref to attach to the component root.\n */\nexport function useDropdownDismiss(open: boolean, onClose: () => void): React.RefObject<HTMLDivElement | null> {\n const rootRef = React.useRef<HTMLDivElement | null>(null)\n React.useEffect(() => {\n if (!open) return\n const handlePointerDown = (event: PointerEvent) => {\n if (rootRef.current && event.target instanceof Node && !rootRef.current.contains(event.target)) onClose()\n }\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n // Swallow Escape so the dialog itself stays open.\n event.stopPropagation()\n onClose()\n }\n }\n document.addEventListener('pointerdown', handlePointerDown, true)\n document.addEventListener('keydown', handleKeyDown, true)\n return () => {\n document.removeEventListener('pointerdown', handlePointerDown, true)\n document.removeEventListener('keydown', handleKeyDown, true)\n }\n }, [open, onClose])\n return rootRef\n}\n\nexport function Field({ label, children, error, className }: { label: string; children: React.ReactNode; error?: string | null; className?: string }) {\n return (\n <div className={cn('flex w-full flex-col gap-1.5', className)}>\n <span className={LABEL_CLASS}>{label}</span>\n {children}\n {error ? <p className=\"text-xs text-status-error-text\">{error}</p> : null}\n </div>\n )\n}\n\nexport function UppercaseBadge({ style, className, children }: { style?: React.CSSProperties; className?: string; children: React.ReactNode }) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full bg-muted px-2 py-0.5 text-overline font-medium uppercase text-muted-foreground',\n className,\n )}\n style={style}\n >\n {children}\n </span>\n )\n}\n\nexport function AllDayToggle({ checked, onCheckedChange, label }: { checked: boolean; onCheckedChange(next: boolean): void; label: string }) {\n return (\n <Switch\n checked={checked}\n aria-label={label}\n onCheckedChange={onCheckedChange}\n className=\"h-6 w-10\"\n />\n )\n}\n\nfunction parseDateValue(value: string): Date | null {\n if (!value) return null\n const parsed = new Date(`${value}T00:00:00`)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n}\n\nfunction formatDateValue(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`\n}\n\n// Close the field's popover when the surrounding editor dialog scrolls, so a\n// portalled DS popover (DatePicker/Select) doesn't float over the form or drift\n// from its field (#3747 feedback). Radix ignores synthetic dismiss events, so\n// this drives the popover's controlled `open` state instead. The editor\n// dispatches `EDITOR_SCROLL_EVENT` on scroll.\nexport const EDITOR_SCROLL_EVENT = 'om-calendar-editor-scroll'\nfunction useCloseOnEditorScroll(setOpen: (open: boolean) => void) {\n React.useEffect(() => {\n const handler = () => setOpen(false)\n document.addEventListener(EDITOR_SCROLL_EVENT, handler)\n return () => document.removeEventListener(EDITOR_SCROLL_EVENT, handler)\n }, [setOpen])\n}\n\n// DS DatePicker (calendar popover) + DS Select (scrolling 30-min list) replace\n// the native `<input type=date|time>` overlays, which only opened via the\n// browser chevron and did not match the design system (#3747 feedback).\nexport function DateControl({\n value,\n onChange,\n ariaLabel,\n className,\n}: {\n value: string\n onChange(next: string): void\n ariaLabel: string\n className?: string\n}) {\n const [open, setOpen] = React.useState(false)\n useCloseOnEditorScroll(setOpen)\n return (\n <DatePicker\n value={parseDateValue(value)}\n onChange={(date) => { if (date) onChange(formatDateValue(date)) }}\n footer=\"none\"\n open={open}\n onOpenChange={setOpen}\n aria-label={ariaLabel}\n className={cn('w-full', className)}\n />\n )\n}\n\n// 24h times at 30-minute steps.\nconst TIME_OPTIONS: string[] = Array.from({ length: 48 }, (_, index) => {\n const hour = Math.floor(index / 2)\n const minute = index % 2 === 0 ? '00' : '30'\n return `${String(hour).padStart(2, '0')}:${minute}`\n})\n\nexport function TimeControl({ value, onChange, ariaLabel }: { value: string; onChange(next: string): void; ariaLabel: string }) {\n const [open, setOpen] = React.useState(false)\n useCloseOnEditorScroll(setOpen)\n // Keep an off-grid value (e.g. an imported 22:15) selectable.\n const options = value && !TIME_OPTIONS.includes(value) ? [value, ...TIME_OPTIONS] : TIME_OPTIONS\n return (\n <Select value={value} onValueChange={onChange} open={open} onOpenChange={setOpen}>\n <SelectTrigger aria-label={ariaLabel} className=\"h-9 w-32 shrink-0\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent className=\"max-h-64\">\n {options.map((time) => (\n <SelectItem key={time} value={time}>{time}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nexport function PersonChip({\n name,\n badge,\n compact,\n onRemove,\n removeLabel,\n}: {\n name: string\n badge?: React.ReactNode\n compact?: boolean\n onRemove?: () => void\n removeLabel?: string\n}) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center gap-1.5 rounded-full bg-muted pl-1 pr-2',\n compact ? 'py-0.5' : 'py-1',\n )}\n >\n <Avatar size=\"xs\" label={name} />\n <span className=\"max-w-40 truncate text-xs font-medium text-foreground\">{name}</span>\n {badge}\n {onRemove ? (\n <IconButton\n variant=\"ghost\"\n size=\"xs\"\n onClick={(event) => { event.stopPropagation(); onRemove() }}\n aria-label={removeLabel}\n className=\"size-5 shrink-0\"\n >\n <Plus aria-hidden className=\"size-3.5 rotate-45 opacity-50\" />\n </IconButton>\n ) : null}\n </span>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,18 @@
1
+ function createVisibleDefinitionScopeClause(scope) {
2
+ const organizationId = scope.organizationId ?? null;
3
+ const tenantId = scope.tenantId ?? null;
4
+ const organizationCandidates = [{ organizationId: null }];
5
+ if (organizationId !== null) organizationCandidates.unshift({ organizationId });
6
+ const tenantCandidates = [{ tenantId: null }];
7
+ if (tenantId !== null) tenantCandidates.unshift({ tenantId });
8
+ return {
9
+ $and: [
10
+ { $or: organizationCandidates },
11
+ { $or: tenantCandidates }
12
+ ]
13
+ };
14
+ }
15
+ export {
16
+ createVisibleDefinitionScopeClause
17
+ };
18
+ //# sourceMappingURL=definition-scope-where.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/entities/lib/definition-scope-where.ts"],
4
+ "sourcesContent": ["export type DefinitionScopeClauseInput = {\n organizationId?: string | null\n tenantId?: string | null\n}\n\n// SQL `IN (?, NULL)` never matches rows where the column IS NULL, so a scope\n// filter that mixes a concrete id with a literal null has to be expressed as an\n// `$or` of equality and IS NULL. Every query that resolves the definitions\n// visible to a scope \u2014 the write path, the validation path, the single-key\n// lookups \u2014 routes through this builder so they cannot drift apart again (#5919).\nexport function createVisibleDefinitionScopeClause(scope: DefinitionScopeClauseInput) {\n const organizationId = scope.organizationId ?? null\n const tenantId = scope.tenantId ?? null\n\n const organizationCandidates = [{ organizationId: null as string | null }]\n if (organizationId !== null) organizationCandidates.unshift({ organizationId })\n\n const tenantCandidates = [{ tenantId: null as string | null }]\n if (tenantId !== null) tenantCandidates.unshift({ tenantId })\n\n return {\n $and: [\n { $or: organizationCandidates },\n { $or: tenantCandidates },\n ],\n }\n}\n"],
5
+ "mappings": "AAUO,SAAS,mCAAmC,OAAmC;AACpF,QAAM,iBAAiB,MAAM,kBAAkB;AAC/C,QAAM,WAAW,MAAM,YAAY;AAEnC,QAAM,yBAAyB,CAAC,EAAE,gBAAgB,KAAsB,CAAC;AACzE,MAAI,mBAAmB,KAAM,wBAAuB,QAAQ,EAAE,eAAe,CAAC;AAE9E,QAAM,mBAAmB,CAAC,EAAE,UAAU,KAAsB,CAAC;AAC7D,MAAI,aAAa,KAAM,kBAAiB,QAAQ,EAAE,SAAS,CAAC;AAE5D,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,EAAE,KAAK,uBAAuB;AAAA,MAC9B,EAAE,KAAK,iBAAiB;AAAA,IAC1B;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -1,5 +1,6 @@
1
1
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
2
2
  import { CustomFieldDef } from "@open-mercato/core/modules/entities/data/entities";
3
+ import { createVisibleDefinitionScopeClause } from "./definition-scope-where.js";
3
4
  function resolveDefinitionScopeFromOrganizationScope(auth, scope) {
4
5
  const authTenantId = auth.tenantId ?? null;
5
6
  const scopeTenantId = scope.tenantId ?? null;
@@ -26,18 +27,11 @@ function createExactDefinitionWhere(entityId, key, scope) {
26
27
  };
27
28
  }
28
29
  function createVisibleDefinitionWhere(entityId, key, scope, options = {}) {
29
- const organizationCandidates = [{ organizationId: null }];
30
- if (scope.organizationId) organizationCandidates.unshift({ organizationId: scope.organizationId });
31
- const tenantCandidates = [{ tenantId: null }];
32
- if (scope.tenantId) tenantCandidates.unshift({ tenantId: scope.tenantId });
33
30
  return {
34
31
  entityId,
35
32
  key,
36
33
  ...options,
37
- $and: [
38
- { $or: organizationCandidates },
39
- { $or: tenantCandidates }
40
- ]
34
+ ...createVisibleDefinitionScopeClause(scope)
41
35
  };
42
36
  }
43
37
  function definitionTimestamp(def) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/definition-scope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\n\ntype AuthScope = {\n tenantId?: string | null\n orgId?: string | null\n}\n\nexport type DefinitionMutationScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\ntype OrganizationScopeLike = {\n tenantId?: string | null\n selectedId?: string | null\n}\n\ntype DefinitionKeySelector = string | { $in: string[] }\n\ntype DefinitionVisibilityOptions = {\n deletedAt?: null | { $ne: null }\n isActive?: boolean\n}\n\ntype DefinitionScopeRecord = {\n tenantId?: string | null\n organizationId?: string | null\n updatedAt?: Date | string | number | null\n createdAt?: Date | string | number | null\n}\n\ntype MutableDefinitionScopeRecord = DefinitionScopeRecord & {\n isActive?: boolean\n deletedAt?: Date | string | number | null\n}\n\nexport function resolveDefinitionScopeFromOrganizationScope(\n auth: AuthScope,\n scope: OrganizationScopeLike,\n): DefinitionMutationScope {\n const authTenantId = auth.tenantId ?? null\n const scopeTenantId = scope.tenantId ?? null\n const tenantMismatch = Boolean(authTenantId && scopeTenantId && authTenantId !== scopeTenantId)\n\n return {\n tenantId: tenantMismatch ? authTenantId : (scopeTenantId ?? authTenantId),\n organizationId: tenantMismatch\n ? (auth.orgId ?? null)\n : (scope.selectedId ?? auth.orgId ?? null),\n }\n}\n\nexport async function resolveDefinitionMutationScope({\n auth,\n container,\n request,\n}: {\n auth: AuthScope\n container: unknown\n request: Request\n}): Promise<DefinitionMutationScope> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request } as any)\n return resolveDefinitionScopeFromOrganizationScope(auth, scope)\n}\n\nexport function createExactDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n) {\n return {\n entityId,\n key,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n}\n\nexport function createVisibleDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n options: DefinitionVisibilityOptions = {},\n) {\n const organizationCandidates = [{ organizationId: null as string | null }]\n if (scope.organizationId) organizationCandidates.unshift({ organizationId: scope.organizationId })\n\n const tenantCandidates = [{ tenantId: null as string | null }]\n if (scope.tenantId) tenantCandidates.unshift({ tenantId: scope.tenantId })\n\n return {\n entityId,\n key,\n ...options,\n $and: [\n { $or: organizationCandidates },\n { $or: tenantCandidates },\n ],\n }\n}\n\nfunction definitionTimestamp(def: DefinitionScopeRecord) {\n const updatedAt = def?.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : (def?.updatedAt ? new Date(def.updatedAt).getTime() : 0)\n if (updatedAt) return updatedAt\n return def?.createdAt instanceof Date\n ? def.createdAt.getTime()\n : (def?.createdAt ? new Date(def.createdAt).getTime() : 0)\n}\n\nfunction definitionScopeScore(def: DefinitionScopeRecord) {\n return (def?.tenantId ? 2 : 0) + (def?.organizationId ? 1 : 0)\n}\n\nexport function selectVisibleDefinitionWinner<T extends DefinitionScopeRecord>(definitions: T[]) {\n let winner: T | null = null\n for (const definition of definitions) {\n if (!winner) {\n winner = definition\n continue\n }\n const nextScore = definitionScopeScore(definition)\n const winnerScore = definitionScopeScore(winner)\n if (nextScore > winnerScore) {\n winner = definition\n continue\n }\n if (nextScore < winnerScore) continue\n if (definitionTimestamp(definition) >= definitionTimestamp(winner)) winner = definition\n }\n return winner\n}\n\nexport function markDefinitionTombstoned<T extends MutableDefinitionScopeRecord>(definition: T, now = new Date()) {\n definition.isActive = false\n definition.deletedAt = definition.deletedAt ?? now\n definition.updatedAt = now\n return definition\n}\n\nfunction cloneConfigJson(configJson: unknown) {\n if (configJson == null) return configJson\n return JSON.parse(JSON.stringify(configJson))\n}\n\nexport function createScopedDefinitionTombstone(\n em: Pick<EntityManager, 'create'>,\n source: {\n entityId: string\n key: string\n kind: string\n configJson?: unknown\n },\n scope: DefinitionMutationScope,\n now = new Date(),\n) {\n return em.create(CustomFieldDef, {\n entityId: source.entityId,\n key: source.key,\n kind: source.kind,\n configJson: cloneConfigJson(source.configJson) ?? {},\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: false,\n createdAt: now,\n updatedAt: now,\n deletedAt: now,\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\nimport { createVisibleDefinitionScopeClause } from './definition-scope-where'\n\ntype AuthScope = {\n tenantId?: string | null\n orgId?: string | null\n}\n\nexport type DefinitionMutationScope = {\n tenantId: string | null\n organizationId: string | null\n}\n\ntype OrganizationScopeLike = {\n tenantId?: string | null\n selectedId?: string | null\n}\n\ntype DefinitionKeySelector = string | { $in: string[] }\n\ntype DefinitionVisibilityOptions = {\n deletedAt?: null | { $ne: null }\n isActive?: boolean\n}\n\ntype DefinitionScopeRecord = {\n tenantId?: string | null\n organizationId?: string | null\n updatedAt?: Date | string | number | null\n createdAt?: Date | string | number | null\n}\n\ntype MutableDefinitionScopeRecord = DefinitionScopeRecord & {\n isActive?: boolean\n deletedAt?: Date | string | number | null\n}\n\nexport function resolveDefinitionScopeFromOrganizationScope(\n auth: AuthScope,\n scope: OrganizationScopeLike,\n): DefinitionMutationScope {\n const authTenantId = auth.tenantId ?? null\n const scopeTenantId = scope.tenantId ?? null\n const tenantMismatch = Boolean(authTenantId && scopeTenantId && authTenantId !== scopeTenantId)\n\n return {\n tenantId: tenantMismatch ? authTenantId : (scopeTenantId ?? authTenantId),\n organizationId: tenantMismatch\n ? (auth.orgId ?? null)\n : (scope.selectedId ?? auth.orgId ?? null),\n }\n}\n\nexport async function resolveDefinitionMutationScope({\n auth,\n container,\n request,\n}: {\n auth: AuthScope\n container: unknown\n request: Request\n}): Promise<DefinitionMutationScope> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request } as any)\n return resolveDefinitionScopeFromOrganizationScope(auth, scope)\n}\n\nexport function createExactDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n) {\n return {\n entityId,\n key,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n}\n\nexport function createVisibleDefinitionWhere(\n entityId: string,\n key: DefinitionKeySelector,\n scope: DefinitionMutationScope,\n options: DefinitionVisibilityOptions = {},\n) {\n return {\n entityId,\n key,\n ...options,\n ...createVisibleDefinitionScopeClause(scope),\n }\n}\n\nfunction definitionTimestamp(def: DefinitionScopeRecord) {\n const updatedAt = def?.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : (def?.updatedAt ? new Date(def.updatedAt).getTime() : 0)\n if (updatedAt) return updatedAt\n return def?.createdAt instanceof Date\n ? def.createdAt.getTime()\n : (def?.createdAt ? new Date(def.createdAt).getTime() : 0)\n}\n\nfunction definitionScopeScore(def: DefinitionScopeRecord) {\n return (def?.tenantId ? 2 : 0) + (def?.organizationId ? 1 : 0)\n}\n\nexport function selectVisibleDefinitionWinner<T extends DefinitionScopeRecord>(definitions: T[]) {\n let winner: T | null = null\n for (const definition of definitions) {\n if (!winner) {\n winner = definition\n continue\n }\n const nextScore = definitionScopeScore(definition)\n const winnerScore = definitionScopeScore(winner)\n if (nextScore > winnerScore) {\n winner = definition\n continue\n }\n if (nextScore < winnerScore) continue\n if (definitionTimestamp(definition) >= definitionTimestamp(winner)) winner = definition\n }\n return winner\n}\n\nexport function markDefinitionTombstoned<T extends MutableDefinitionScopeRecord>(definition: T, now = new Date()) {\n definition.isActive = false\n definition.deletedAt = definition.deletedAt ?? now\n definition.updatedAt = now\n return definition\n}\n\nfunction cloneConfigJson(configJson: unknown) {\n if (configJson == null) return configJson\n return JSON.parse(JSON.stringify(configJson))\n}\n\nexport function createScopedDefinitionTombstone(\n em: Pick<EntityManager, 'create'>,\n source: {\n entityId: string\n key: string\n kind: string\n configJson?: unknown\n },\n scope: DefinitionMutationScope,\n now = new Date(),\n) {\n return em.create(CustomFieldDef, {\n entityId: source.entityId,\n key: source.key,\n kind: source.kind,\n configJson: cloneConfigJson(source.configJson) ?? {},\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: false,\n createdAt: now,\n updatedAt: now,\n deletedAt: now,\n })\n}\n"],
5
+ "mappings": "AACA,SAAS,0CAA0C;AACnD,SAAS,sBAAsB;AAC/B,SAAS,0CAA0C;AAoC5C,SAAS,4CACd,MACA,OACyB;AACzB,QAAM,eAAe,KAAK,YAAY;AACtC,QAAM,gBAAgB,MAAM,YAAY;AACxC,QAAM,iBAAiB,QAAQ,gBAAgB,iBAAiB,iBAAiB,aAAa;AAE9F,SAAO;AAAA,IACL,UAAU,iBAAiB,eAAgB,iBAAiB;AAAA,IAC5D,gBAAgB,iBACX,KAAK,SAAS,OACd,MAAM,cAAc,KAAK,SAAS;AAAA,EACzC;AACF;AAEA,eAAsB,+BAA+B;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AACF,GAIqC;AACnC,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,QAAQ,CAAQ;AAC1F,SAAO,4CAA4C,MAAM,KAAK;AAChE;AAEO,SAAS,2BACd,UACA,KACA,OACA;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,EAClB;AACF;AAEO,SAAS,6BACd,UACA,KACA,OACA,UAAuC,CAAC,GACxC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAG;AAAA,IACH,GAAG,mCAAmC,KAAK;AAAA,EAC7C;AACF;AAEA,SAAS,oBAAoB,KAA4B;AACvD,QAAM,YAAY,KAAK,qBAAqB,OACxC,IAAI,UAAU,QAAQ,IACrB,KAAK,YAAY,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ,IAAI;AAC1D,MAAI,UAAW,QAAO;AACtB,SAAO,KAAK,qBAAqB,OAC7B,IAAI,UAAU,QAAQ,IACrB,KAAK,YAAY,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ,IAAI;AAC5D;AAEA,SAAS,qBAAqB,KAA4B;AACxD,UAAQ,KAAK,WAAW,IAAI,MAAM,KAAK,iBAAiB,IAAI;AAC9D;AAEO,SAAS,8BAA+D,aAAkB;AAC/F,MAAI,SAAmB;AACvB,aAAW,cAAc,aAAa;AACpC,QAAI,CAAC,QAAQ;AACX,eAAS;AACT;AAAA,IACF;AACA,UAAM,YAAY,qBAAqB,UAAU;AACjD,UAAM,cAAc,qBAAqB,MAAM;AAC/C,QAAI,YAAY,aAAa;AAC3B,eAAS;AACT;AAAA,IACF;AACA,QAAI,YAAY,YAAa;AAC7B,QAAI,oBAAoB,UAAU,KAAK,oBAAoB,MAAM,EAAG,UAAS;AAAA,EAC/E;AACA,SAAO;AACT;AAEO,SAAS,yBAAiE,YAAe,MAAM,oBAAI,KAAK,GAAG;AAChH,aAAW,WAAW;AACtB,aAAW,YAAY,WAAW,aAAa;AAC/C,aAAW,YAAY;AACvB,SAAO;AACT;AAEA,SAAS,gBAAgB,YAAqB;AAC5C,MAAI,cAAc,KAAM,QAAO;AAC/B,SAAO,KAAK,MAAM,KAAK,UAAU,UAAU,CAAC;AAC9C;AAEO,SAAS,gCACd,IACA,QAMA,OACA,MAAM,oBAAI,KAAK,GACf;AACA,SAAO,GAAG,OAAO,gBAAgB;AAAA,IAC/B,UAAU,OAAO;AAAA,IACjB,KAAK,OAAO;AAAA,IACZ,MAAM,OAAO;AAAA,IACb,YAAY,gBAAgB,OAAO,UAAU,KAAK,CAAC;AAAA,IACnD,gBAAgB,MAAM;AAAA,IACtB,UAAU,MAAM;AAAA,IAChB,UAAU;AAAA,IACV,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -3,7 +3,8 @@ import {
3
3
  MAX_CUSTOM_FIELD_KEYS_PER_RECORD,
4
4
  TOO_MANY_CUSTOM_FIELDS_ERROR
5
5
  } from "@open-mercato/shared/modules/entities/validation";
6
- import { CustomFieldDef, CustomFieldValue } from "../data/entities.js";
6
+ import { CustomFieldValue } from "../data/entities.js";
7
+ import { loadScopedCustomFieldDefs } from "./scoped-field-defs.js";
7
8
  function columnFromKind(kind) {
8
9
  switch (kind) {
9
10
  case "text":
@@ -44,34 +45,7 @@ async function setRecordCustomFields(em, opts) {
44
45
  const preferDefs = opts.preferDefs !== false;
45
46
  let defsByKey;
46
47
  if (preferDefs) {
47
- const defs = await em.find(CustomFieldDef, {
48
- entityId,
49
- isActive: true,
50
- deletedAt: null,
51
- organizationId: { $in: [organizationId, null] },
52
- tenantId: { $in: [tenantId, null] }
53
- });
54
- const scopeScore = (def) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0);
55
- defsByKey = {};
56
- for (const d of defs) {
57
- const existing = defsByKey[d.key];
58
- if (!existing) {
59
- defsByKey[d.key] = d;
60
- continue;
61
- }
62
- const nextScore = scopeScore(d);
63
- const existingScore = scopeScore(existing);
64
- if (nextScore > existingScore) {
65
- defsByKey[d.key] = d;
66
- continue;
67
- }
68
- if (nextScore < existingScore) continue;
69
- const nextUpdatedAt = d.updatedAt instanceof Date ? d.updatedAt.getTime() : new Date(d.updatedAt).getTime();
70
- const existingUpdatedAt = existing.updatedAt instanceof Date ? existing.updatedAt.getTime() : new Date(existing.updatedAt).getTime();
71
- if (nextUpdatedAt >= existingUpdatedAt) {
72
- defsByKey[d.key] = d;
73
- }
74
- }
48
+ defsByKey = await loadScopedCustomFieldDefs(em, { entityId, organizationId, tenantId });
75
49
  }
76
50
  const toPersist = [];
77
51
  let encryptionService;
@@ -94,7 +68,7 @@ async function setRecordCustomFields(em, opts) {
94
68
  for (const fieldKey of keys) {
95
69
  const raw = values[fieldKey];
96
70
  if (raw === void 0) continue;
97
- const def = defsByKey?.[fieldKey];
71
+ const def = defsByKey?.get(fieldKey);
98
72
  const encrypted = Boolean(def?.configJson && def.configJson?.encrypted);
99
73
  const isArray = Array.isArray(raw);
100
74
  if (isArray) {
@@ -104,7 +78,7 @@ async function setRecordCustomFields(em, opts) {
104
78
  const col = encrypted ? "valueText" : def ? columnFromKind(def.kind) : columnFromJsValue(val);
105
79
  const cf2 = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: /* @__PURE__ */ new Date() });
106
80
  clearValueColumns(cf2);
107
- const stored = encrypted ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache) : val;
81
+ const stored = encrypted ? await encryptCustomFieldValue(val, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey }) : val;
108
82
  switch (col) {
109
83
  case "valueText":
110
84
  cf2.valueText = stored == null ? null : String(stored);
@@ -130,7 +104,7 @@ async function setRecordCustomFields(em, opts) {
130
104
  continue;
131
105
  }
132
106
  const column = encrypted ? "valueText" : def ? columnFromKind(def.kind) : columnFromJsValue(raw);
133
- const storedValue = encrypted ? await encryptCustomFieldValue(raw, tenantId, getEncryptionService(), encryptionCache) : raw;
107
+ const storedValue = encrypted ? await encryptCustomFieldValue(raw, tenantId, getEncryptionService(), encryptionCache, { entityId, fieldKey }) : raw;
134
108
  let cf = await em.findOne(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey });
135
109
  if (!cf) {
136
110
  cf = em.create(CustomFieldValue, { entityId, recordId, organizationId, tenantId, fieldKey, createdAt: /* @__PURE__ */ new Date() });
@@ -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 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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",
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'\nimport { loadScopedCustomFieldDefs } from './scoped-field-defs'\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: Map<string, CustomFieldDef> | undefined\n if (preferDefs) {\n defsByKey = await loadScopedCustomFieldDefs(em, { entityId, organizationId, tenantId })\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?.get(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, { entityId, fieldKey })\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, { entityId, fieldKey })\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
  }
@@ -1,21 +1,16 @@
1
1
  import { CustomFieldDef } from "../data/entities.js";
2
+ import { createVisibleDefinitionScopeClause } from "./definition-scope-where.js";
2
3
  const scopeScore = (def) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0);
3
4
  const definitionTime = (def) => def.updatedAt instanceof Date ? def.updatedAt.getTime() : new Date(def.updatedAt).getTime();
4
5
  async function loadScopedCustomFieldDefs(em, opts) {
5
- const organizationId = opts.organizationId ?? null;
6
- const tenantId = opts.tenantId ?? null;
7
6
  const defs = await em.find(CustomFieldDef, {
8
7
  entityId: opts.entityId,
9
8
  isActive: true,
10
9
  deletedAt: null,
11
- $and: [
12
- {
13
- $or: organizationId === null ? [{ organizationId: null }] : [{ organizationId }, { organizationId: null }]
14
- },
15
- {
16
- $or: tenantId === null ? [{ tenantId: null }] : [{ tenantId }, { tenantId: null }]
17
- }
18
- ]
10
+ ...createVisibleDefinitionScopeClause({
11
+ organizationId: opts.organizationId,
12
+ tenantId: opts.tenantId
13
+ })
19
14
  });
20
15
  const byKey = /* @__PURE__ */ new Map();
21
16
  for (const def of defs) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/scoped-field-defs.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef } from '../data/entities'\n\nexport type ScopedCustomFieldDefsOptions = {\n entityId: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\n// Definitions overlay by scope: an org+tenant row shadows a tenant-global row,\n// which shadows an instance-global one. Duplicate keys within the same scope are\n// resolved by recency so the newest definition wins.\nconst scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n\nconst definitionTime = (def: CustomFieldDef) =>\n def.updatedAt instanceof Date ? def.updatedAt.getTime() : new Date(def.updatedAt).getTime()\n\nexport async function loadScopedCustomFieldDefs(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, CustomFieldDef>> {\n const organizationId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const defs = await em.find(CustomFieldDef, {\n entityId: opts.entityId,\n isActive: true,\n deletedAt: null,\n $and: [\n {\n $or: organizationId === null\n ? [{ organizationId: null }]\n : [{ organizationId }, { organizationId: null }],\n },\n {\n $or: tenantId === null\n ? [{ tenantId: null }]\n : [{ tenantId }, { tenantId: null }],\n },\n ],\n } as any)\n\n const byKey = new Map<string, CustomFieldDef>()\n for (const def of defs) {\n const existing = byKey.get(def.key)\n if (!existing) {\n byKey.set(def.key, def)\n continue\n }\n const nextScore = scopeScore(def)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n byKey.set(def.key, def)\n continue\n }\n if (nextScore < existingScore) continue\n if (definitionTime(def) >= definitionTime(existing)) byKey.set(def.key, def)\n }\n return byKey\n}\n\n// Kind lookup used by read paths that must not reinterpret a stored value against\n// the wrong type. Failures degrade to an empty map: callers then leave values as\n// stored, which is always safer than coercing them blind.\nexport async function loadCustomFieldKinds(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, string>> {\n try {\n const defs = await loadScopedCustomFieldDefs(em, opts)\n const kinds = new Map<string, string>()\n for (const [key, def] of defs) kinds.set(key, String(def.kind || ''))\n return kinds\n } catch {\n return new Map<string, string>()\n }\n}\n"],
5
- "mappings": "AACA,SAAS,sBAAsB;AAW/B,MAAM,aAAa,CAAC,SAAyB,IAAI,WAAW,IAAI,MAAM,IAAI,iBAAiB,IAAI;AAE/F,MAAM,iBAAiB,CAAC,QACtB,IAAI,qBAAqB,OAAO,IAAI,UAAU,QAAQ,IAAI,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ;AAE5F,eAAsB,0BACpB,IACA,MACsC;AACtC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,OAAO,MAAM,GAAG,KAAK,gBAAgB;AAAA,IACzC,UAAU,KAAK;AAAA,IACf,UAAU;AAAA,IACV,WAAW;AAAA,IACX,MAAM;AAAA,MACJ;AAAA,QACE,KAAK,mBAAmB,OACpB,CAAC,EAAE,gBAAgB,KAAK,CAAC,IACzB,CAAC,EAAE,eAAe,GAAG,EAAE,gBAAgB,KAAK,CAAC;AAAA,MACnD;AAAA,MACA;AAAA,QACE,KAAK,aAAa,OACd,CAAC,EAAE,UAAU,KAAK,CAAC,IACnB,CAAC,EAAE,SAAS,GAAG,EAAE,UAAU,KAAK,CAAC;AAAA,MACvC;AAAA,IACF;AAAA,EACF,CAAQ;AAER,QAAM,QAAQ,oBAAI,IAA4B;AAC9C,aAAW,OAAO,MAAM;AACtB,UAAM,WAAW,MAAM,IAAI,IAAI,GAAG;AAClC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,UAAM,YAAY,WAAW,GAAG;AAChC,UAAM,gBAAgB,WAAW,QAAQ;AACzC,QAAI,YAAY,eAAe;AAC7B,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,QAAI,YAAY,cAAe;AAC/B,QAAI,eAAe,GAAG,KAAK,eAAe,QAAQ,EAAG,OAAM,IAAI,IAAI,KAAK,GAAG;AAAA,EAC7E;AACA,SAAO;AACT;AAKA,eAAsB,qBACpB,IACA,MAC8B;AAC9B,MAAI;AACF,UAAM,OAAO,MAAM,0BAA0B,IAAI,IAAI;AACrD,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,CAAC,KAAK,GAAG,KAAK,KAAM,OAAM,IAAI,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC;AACpE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,oBAAI,IAAoB;AAAA,EACjC;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef } from '../data/entities'\nimport { createVisibleDefinitionScopeClause } from './definition-scope-where'\n\nexport type ScopedCustomFieldDefsOptions = {\n entityId: string\n organizationId?: string | null\n tenantId?: string | null\n}\n\n// Definitions overlay by scope: an org+tenant row shadows a tenant-global row,\n// which shadows an instance-global one. Duplicate keys within the same scope are\n// resolved by recency so the newest definition wins.\nconst scopeScore = (def: CustomFieldDef) => (def.tenantId ? 2 : 0) + (def.organizationId ? 1 : 0)\n\nconst definitionTime = (def: CustomFieldDef) =>\n def.updatedAt instanceof Date ? def.updatedAt.getTime() : new Date(def.updatedAt).getTime()\n\nexport async function loadScopedCustomFieldDefs(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, CustomFieldDef>> {\n const defs = await em.find(CustomFieldDef, {\n entityId: opts.entityId,\n isActive: true,\n deletedAt: null,\n ...createVisibleDefinitionScopeClause({\n organizationId: opts.organizationId,\n tenantId: opts.tenantId,\n }),\n })\n\n const byKey = new Map<string, CustomFieldDef>()\n for (const def of defs) {\n const existing = byKey.get(def.key)\n if (!existing) {\n byKey.set(def.key, def)\n continue\n }\n const nextScore = scopeScore(def)\n const existingScore = scopeScore(existing)\n if (nextScore > existingScore) {\n byKey.set(def.key, def)\n continue\n }\n if (nextScore < existingScore) continue\n if (definitionTime(def) >= definitionTime(existing)) byKey.set(def.key, def)\n }\n return byKey\n}\n\n// Kind lookup used by read paths that must not reinterpret a stored value against\n// the wrong type. Failures degrade to an empty map: callers then leave values as\n// stored, which is always safer than coercing them blind.\nexport async function loadCustomFieldKinds(\n em: EntityManager,\n opts: ScopedCustomFieldDefsOptions,\n): Promise<Map<string, string>> {\n try {\n const defs = await loadScopedCustomFieldDefs(em, opts)\n const kinds = new Map<string, string>()\n for (const [key, def] of defs) kinds.set(key, String(def.kind || ''))\n return kinds\n } catch {\n return new Map<string, string>()\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,sBAAsB;AAC/B,SAAS,0CAA0C;AAWnD,MAAM,aAAa,CAAC,SAAyB,IAAI,WAAW,IAAI,MAAM,IAAI,iBAAiB,IAAI;AAE/F,MAAM,iBAAiB,CAAC,QACtB,IAAI,qBAAqB,OAAO,IAAI,UAAU,QAAQ,IAAI,IAAI,KAAK,IAAI,SAAS,EAAE,QAAQ;AAE5F,eAAsB,0BACpB,IACA,MACsC;AACtC,QAAM,OAAO,MAAM,GAAG,KAAK,gBAAgB;AAAA,IACzC,UAAU,KAAK;AAAA,IACf,UAAU;AAAA,IACV,WAAW;AAAA,IACX,GAAG,mCAAmC;AAAA,MACpC,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,IACjB,CAAC;AAAA,EACH,CAAC;AAED,QAAM,QAAQ,oBAAI,IAA4B;AAC9C,aAAW,OAAO,MAAM;AACtB,UAAM,WAAW,MAAM,IAAI,IAAI,GAAG;AAClC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,UAAM,YAAY,WAAW,GAAG;AAChC,UAAM,gBAAgB,WAAW,QAAQ;AACzC,QAAI,YAAY,eAAe;AAC7B,YAAM,IAAI,IAAI,KAAK,GAAG;AACtB;AAAA,IACF;AACA,QAAI,YAAY,cAAe;AAC/B,QAAI,eAAe,GAAG,KAAK,eAAe,QAAQ,EAAG,OAAM,IAAI,IAAI,KAAK,GAAG;AAAA,EAC7E;AACA,SAAO;AACT;AAKA,eAAsB,qBACpB,IACA,MAC8B;AAC9B,MAAI;AACF,UAAM,OAAO,MAAM,0BAA0B,IAAI,IAAI;AACrD,UAAM,QAAQ,oBAAI,IAAoB;AACtC,eAAW,CAAC,KAAK,GAAG,KAAK,KAAM,OAAM,IAAI,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC;AACpE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,oBAAI,IAAoB;AAAA,EACjC;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { 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 } from "./document.js";
4
+ import { buildIndexDocument, rebuildAggregateSearchField } from "./document.js";
5
5
  import { replaceSearchTokensForBatch, isSearchDebugEnabled } from "./search-tokens.js";
6
6
  import { createLogger } from "@open-mercato/shared/lib/logger";
7
7
  import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
@@ -169,7 +169,7 @@ async function upsertIndexBatch(db, entityType, rows, scope, options = {}) {
169
169
  tenantId: scopeTenant ?? null
170
170
  });
171
171
  if (decrypted && typeof decrypted === "object") {
172
- tokenDoc = decrypted;
172
+ tokenDoc = rebuildAggregateSearchField(decrypted, { entityType, config: searchConfig });
173
173
  }
174
174
  } catch (decryptError) {
175
175
  logger.warn("Failed to decrypt index document for search tokens", { entityType, recordId });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/batch.ts"],
4
- "sourcesContent": ["import { type Kysely, sql } from 'kysely'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { isUniqueViolation } from '@open-mercato/shared/lib/db/pg-errors'\nimport { buildIndexDocument, type IndexCustomFieldValue } from './document'\nimport { replaceSearchTokensForBatch, isSearchDebugEnabled } from './search-tokens'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\n\nconst logger = createLogger('query_index').child({ component: 'reindex-batch' })\n\n/**\n * Cap on how many per-row failures a single batch persists to `indexer_error_logs`.\n * Under a systemic failure (pool exhausted, disk full, KMS down) every row fails, and\n * writing one error row per record would hammer the database that is already failing.\n * The count of suppressed failures still reaches the caller via `failedRecordIds`.\n */\nconst MAX_RECORDED_ROW_ERRORS_PER_BATCH = 50\n\nexport type AnyRow = Record<string, any> & { id: string | number }\n\nexport type ScopeOverrides = {\n orgId?: string\n tenantId?: string\n}\n\n/**\n * What a batch actually wrote, as opposed to what it was asked to write. Callers MUST\n * reconcile `written` against `attempted` \u2014 a batch that loses rows and reports success\n * is how records go permanently missing from the index (issue GSM-266).\n */\nexport type UpsertIndexBatchResult = {\n attempted: number\n written: number\n failedRecordIds: string[]\n searchTokenFailures: number\n}\n\nexport class QueryIndexBatchWriteError extends Error {\n readonly entityType: string\n readonly attempted: number\n readonly written: number\n readonly failedRecordIds: string[]\n\n constructor(entityType: string, result: UpsertIndexBatchResult) {\n const preview = result.failedRecordIds.slice(0, 10)\n const suffix = result.failedRecordIds.length > preview.length ? ', ...' : ''\n super(\n `[internal] Query index write incomplete for ${entityType}: wrote ${result.written} of ${result.attempted} records` +\n (preview.length ? ` (failed: ${preview.join(', ')}${suffix})` : ''),\n )\n this.name = 'QueryIndexBatchWriteError'\n this.entityType = entityType\n this.attempted = result.attempted\n this.written = result.written\n this.failedRecordIds = result.failedRecordIds\n }\n}\n\nexport function assertIndexBatchWritesLanded(entityType: string, result: UpsertIndexBatchResult): void {\n if (result.written >= result.attempted && result.failedRecordIds.length === 0) return\n throw new QueryIndexBatchWriteError(entityType, result)\n}\n\nexport function createEmptyUpsertIndexBatchResult(): UpsertIndexBatchResult {\n return { attempted: 0, written: 0, failedRecordIds: [], searchTokenFailures: 0 }\n}\n\nexport function mergeUpsertIndexBatchResults(\n target: UpsertIndexBatchResult,\n addition: UpsertIndexBatchResult,\n): UpsertIndexBatchResult {\n target.attempted += addition.attempted\n target.written += addition.written\n target.searchTokenFailures += addition.searchTokenFailures\n target.failedRecordIds.push(...addition.failedRecordIds)\n return target\n}\n\ntype CustomFieldRow = {\n record_id: string\n field_key: string\n value_text: string | null\n value_multiline: string | null\n value_int: number | null\n value_float: number | null\n value_bool: boolean | null\n organization_id: string | null\n tenant_id: string | null\n}\n\nexport type IndexBatchOptions = {\n deriveOrganizationId?: (row: AnyRow) => string | null | undefined\n encryptDoc?: (\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => Promise<Record<string, unknown> | null | undefined>\n decryptDoc?: (\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => Promise<Record<string, unknown> | null | undefined>\n}\n\nfunction normalizeId(value: unknown): string {\n return String(value)\n}\n\nfunction normalizeScopedValue(value: unknown): string | null {\n if (value === undefined || value === null || value === '') return null\n return String(value)\n}\n\ntype IndexRowPayload = {\n entity_type: string\n entity_id: string\n organization_id: string | null\n tenant_id: string | null\n doc: Record<string, unknown>\n tokenDoc: Record<string, unknown>\n index_version: number\n}\n\nasync function updateIndexRow(db: Kysely<any>, payload: IndexRowPayload): Promise<number> {\n let updateQuery = db\n .updateTable('entity_indexes' as any)\n .set({\n doc: sql`${JSON.stringify(payload.doc)}::jsonb`,\n index_version: payload.index_version,\n organization_id: payload.organization_id ?? null,\n tenant_id: payload.tenant_id ?? null,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .where('entity_type' as any, '=', payload.entity_type)\n .where('entity_id' as any, '=', payload.entity_id)\n updateQuery = payload.organization_id == null\n ? updateQuery.where('organization_id' as any, 'is', null as any)\n : updateQuery.where('organization_id' as any, '=', payload.organization_id)\n const result = await updateQuery.executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n return Number(result?.numUpdatedRows ?? 0)\n}\n\nexport async function upsertIndexBatch(\n db: Kysely<any>,\n entityType: string,\n rows: AnyRow[],\n scope: ScopeOverrides,\n options: IndexBatchOptions = {},\n): Promise<UpsertIndexBatchResult> {\n if (!rows.length) return createEmptyUpsertIndexBatchResult()\n const recordIds = rows.map((row) => normalizeId(row.id))\n\n const failedRecordIds: string[] = []\n let searchTokenFailures = 0\n let recordedRowErrors = 0\n\n const recordRowFailure = async (\n recordId: string,\n error: unknown,\n handler: string,\n rowScope: { organizationId: string | null; tenantId: string | null },\n ): Promise<void> => {\n failedRecordIds.push(recordId)\n if (recordedRowErrors >= MAX_RECORDED_ROW_ERRORS_PER_BATCH) return\n recordedRowErrors += 1\n await recordIndexerError(\n { db },\n {\n source: 'query_index',\n handler,\n error,\n entityType,\n recordId,\n tenantId: rowScope.tenantId,\n organizationId: rowScope.organizationId,\n },\n ).catch(() => undefined)\n }\n\n const shouldMergeCustomerEntity =\n entityType === 'customers:customer_person_profile' || entityType === 'customers:customer_company_profile'\n\n let customerEntitiesById: Map<string, AnyRow> | null = null\n if (shouldMergeCustomerEntity) {\n const entityIds = Array.from(\n new Set(\n rows\n .map((row) => (row as AnyRow).entity_id || (row as AnyRow).entityId)\n .filter((value): value is string | number => value !== undefined && value !== null && `${value}` !== '')\n .map((value) => normalizeId(value)),\n ),\n )\n if (entityIds.length) {\n const entityRows = await db\n .selectFrom('customer_entities' as any)\n .selectAll()\n .where('id' as any, 'in', entityIds)\n .execute() as AnyRow[]\n customerEntitiesById = new Map(entityRows.map((row) => [normalizeId(row.id), row]))\n }\n }\n\n const customFieldRows = await db\n .selectFrom('custom_field_values' as any)\n .select([\n 'record_id' as any,\n 'field_key' as any,\n 'value_text' as any,\n 'value_multiline' as any,\n 'value_int' as any,\n 'value_float' as any,\n 'value_bool' as any,\n 'organization_id' as any,\n 'tenant_id' as any,\n ])\n .where('entity_id' as any, '=', entityType)\n .where('record_id' as any, 'in', recordIds)\n .execute() as CustomFieldRow[]\n\n const customFieldMap = new Map<string, CustomFieldRow[]>()\n for (const fieldRow of customFieldRows) {\n const key = normalizeId(fieldRow.record_id)\n const bucket = customFieldMap.get(key)\n if (bucket) bucket.push(fieldRow)\n else customFieldMap.set(key, [fieldRow])\n }\n\n const basePayloads: IndexRowPayload[] = []\n\n const debugEnabled = isSearchDebugEnabled()\n const searchConfig = resolveSearchConfig()\n\n for (const row of rows) {\n const recordId = normalizeId(row.id)\n const baseOrg = normalizeScopedValue((row as AnyRow).organization_id)\n const baseTenant = normalizeScopedValue((row as AnyRow).tenant_id)\n const derivedOrg = options?.deriveOrganizationId\n ? normalizeScopedValue(options.deriveOrganizationId(row))\n : undefined\n const scopeOrg =\n scope.orgId !== undefined\n ? scope.orgId\n : derivedOrg !== undefined\n ? derivedOrg\n : baseOrg\n const scopeTenant = scope.tenantId !== undefined ? scope.tenantId : baseTenant\n const inputRows = customFieldMap.get(recordId) ?? []\n const values: IndexCustomFieldValue[] = inputRows.map((fieldRow) => ({\n key: fieldRow.field_key,\n value:\n fieldRow.value_bool ??\n fieldRow.value_int ??\n fieldRow.value_float ??\n fieldRow.value_text ??\n fieldRow.value_multiline ??\n null,\n organizationId: normalizeScopedValue(fieldRow.organization_id),\n tenantId: normalizeScopedValue(fieldRow.tenant_id),\n }))\n const mergedRow = (() => {\n if (!shouldMergeCustomerEntity || !customerEntitiesById) return row\n const entityId = (row as AnyRow).entity_id || (row as AnyRow).entityId\n if (!entityId) return row\n const entityRow = customerEntitiesById.get(normalizeId(entityId))\n if (!entityRow) return row\n return { ...entityRow, ...row }\n })()\n let doc = buildIndexDocument(\n mergedRow,\n values,\n {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n },\n { entityType, config: searchConfig },\n )\n let tokenDoc: Record<string, unknown> = doc\n if (typeof options.encryptDoc === 'function') {\n try {\n const encrypted = await options.encryptDoc(entityType, doc, {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n })\n if (encrypted && typeof encrypted === 'object') {\n doc = encrypted\n tokenDoc = encrypted\n }\n } catch (encryptError) {\n // Falling through would write the plaintext document into entity_indexes,\n // which must stay encrypted at rest. Skip the row instead so the previously\n // encrypted row survives, and let the caller fail the run.\n await recordRowFailure(recordId, encryptError, 'query_index:reindex-batch:encrypt', {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n })\n continue\n }\n }\n if (typeof options.decryptDoc === 'function') {\n try {\n const decrypted = await options.decryptDoc(entityType, doc, {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n })\n if (decrypted && typeof decrypted === 'object') {\n tokenDoc = decrypted\n }\n } catch (decryptError) {\n // Only affects the search tokens built below; the indexed document itself is\n // unharmed, so the row still counts as written.\n logger.warn('Failed to decrypt index document for search tokens', { entityType, recordId })\n await recordIndexerError(\n { db },\n {\n source: 'fulltext',\n handler: 'query_index:reindex-batch:decrypt',\n error: decryptError,\n entityType,\n recordId,\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n },\n ).catch(() => undefined)\n }\n }\n basePayloads.push({\n entity_type: entityType,\n entity_id: recordId,\n organization_id: scopeOrg ?? null,\n tenant_id: scopeTenant ?? null,\n doc,\n tokenDoc,\n index_version: 1,\n })\n if (debugEnabled) {\n const sample = {\n display_name: (tokenDoc as any).display_name,\n first_name: (tokenDoc as any).first_name,\n last_name: (tokenDoc as any).last_name,\n brand_name: (tokenDoc as any).brand_name,\n legal_name: (tokenDoc as any).legal_name,\n }\n logger.debug('Reindex batch document', {\n entityType,\n recordId,\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n sample,\n })\n }\n }\n\n const insertRows = basePayloads.map((payload) => ({\n entity_type: payload.entity_type,\n entity_id: payload.entity_id,\n organization_id: payload.organization_id,\n tenant_id: payload.tenant_id,\n doc: sql`${JSON.stringify(payload.doc)}::jsonb`,\n index_version: payload.index_version,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n }))\n\n const tokenPayloads = basePayloads.map((payload) => ({\n entityType: payload.entity_type,\n recordId: payload.entity_id,\n organizationId: payload.organization_id,\n tenantId: payload.tenant_id,\n doc: payload.tokenDoc,\n }))\n\n const writeSearchTokens = async (payloads = tokenPayloads): Promise<void> => {\n if (!payloads.length) return\n try {\n await replaceSearchTokensForBatch(db, payloads)\n } catch (searchTokenError) {\n // Record instead of swallowing: a failed token write leaves fulltext search stale.\n // Not counted as a write failure \u2014 replaceSearchTokensForBatch is transactional, so\n // tokens end up stale rather than missing, and the record's next write rebuilds them.\n searchTokenFailures += 1\n await recordIndexerError(\n { db },\n { source: 'fulltext', handler: 'query_index:reindex-batch', error: searchTokenError, entityType, tenantId: scope.tenantId ?? null, organizationId: scope.orgId ?? null },\n ).catch(() => undefined)\n }\n }\n\n const buildResult = (written: number): UpsertIndexBatchResult => ({\n attempted: rows.length,\n written,\n failedRecordIds,\n searchTokenFailures,\n })\n\n if (!basePayloads.length) return buildResult(0)\n\n let bulkWriteFailed = false\n try {\n // One statement, so it is atomic on its own. Note the arbiter is\n // (entity_type, entity_id, organization_id_coalesced): two payloads sharing a\n // conflict key in the same batch would raise 21000 for the whole statement and\n // fall through to the per-row path below.\n await db\n .insertInto('entity_indexes' as any)\n .values(insertRows as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'entity_id', 'organization_id_coalesced'])\n .doUpdateSet({\n doc: sql`excluded.doc`,\n index_version: sql`excluded.index_version`,\n organization_id: sql`excluded.organization_id`,\n tenant_id: sql`excluded.tenant_id`,\n deleted_at: sql`excluded.deleted_at`,\n updated_at: sql`now()`,\n } as any))\n .execute()\n } catch (bulkError) {\n bulkWriteFailed = true\n logger.warn('Bulk index upsert failed; falling back to per-row writes', {\n entityType,\n records: basePayloads.length,\n })\n await recordIndexerError(\n { db },\n {\n source: 'query_index',\n handler: 'query_index:reindex-batch:bulk',\n error: bulkError,\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.orgId ?? null,\n },\n ).catch(() => undefined)\n }\n\n if (!bulkWriteFailed) {\n await writeSearchTokens()\n if (debugEnabled) {\n logger.debug('Reindex batch tokens', {\n entityType,\n records: basePayloads.length,\n scopeOrg: scope.orgId ?? null,\n scopeTenant: scope.tenantId ?? null,\n })\n }\n return buildResult(basePayloads.length)\n }\n\n // Deliberately NOT wrapped in a transaction. In Postgres a single failing statement\n // aborts the surrounding transaction, and COMMIT on an aborted transaction answers\n // with a ROLLBACK tag instead of raising \u2014 so one bad row used to discard the entire\n // batch while this function reported success (issue GSM-266). Each row here is an\n // independent, idempotent upsert; they share no invariant worth a transaction.\n let written = 0\n for (const payload of basePayloads) {\n try {\n if (await updateIndexRow(db, payload) > 0) {\n written += 1\n continue\n }\n try {\n await db\n .insertInto('entity_indexes' as any)\n .values({\n entity_type: payload.entity_type,\n entity_id: payload.entity_id,\n organization_id: payload.organization_id,\n tenant_id: payload.tenant_id,\n doc: sql`${JSON.stringify(payload.doc)}::jsonb`,\n index_version: payload.index_version,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .execute()\n written += 1\n } catch (insertError) {\n // A concurrent worker may have inserted the row between our UPDATE and INSERT.\n // Only a successful retry of the UPDATE proves the row is actually there.\n if (isUniqueViolation(insertError) && await updateIndexRow(db, payload) > 0) {\n written += 1\n continue\n }\n await recordRowFailure(payload.entity_id, insertError, 'query_index:reindex-batch:row', {\n organizationId: payload.organization_id,\n tenantId: payload.tenant_id,\n })\n }\n } catch (rowError) {\n await recordRowFailure(payload.entity_id, rowError, 'query_index:reindex-batch:row', {\n organizationId: payload.organization_id,\n tenantId: payload.tenant_id,\n })\n }\n }\n\n const failedRecordIdSet = new Set(failedRecordIds)\n await writeSearchTokens(tokenPayloads.filter((payload) => !failedRecordIdSet.has(payload.recordId)))\n return buildResult(written)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type Kysely, sql } from 'kysely'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { isUniqueViolation } from '@open-mercato/shared/lib/db/pg-errors'\nimport { buildIndexDocument, rebuildAggregateSearchField, type IndexCustomFieldValue } from './document'\nimport { replaceSearchTokensForBatch, isSearchDebugEnabled } from './search-tokens'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\n\nconst logger = createLogger('query_index').child({ component: 'reindex-batch' })\n\n/**\n * Cap on how many per-row failures a single batch persists to `indexer_error_logs`.\n * Under a systemic failure (pool exhausted, disk full, KMS down) every row fails, and\n * writing one error row per record would hammer the database that is already failing.\n * The count of suppressed failures still reaches the caller via `failedRecordIds`.\n */\nconst MAX_RECORDED_ROW_ERRORS_PER_BATCH = 50\n\nexport type AnyRow = Record<string, any> & { id: string | number }\n\nexport type ScopeOverrides = {\n orgId?: string\n tenantId?: string\n}\n\n/**\n * What a batch actually wrote, as opposed to what it was asked to write. Callers MUST\n * reconcile `written` against `attempted` \u2014 a batch that loses rows and reports success\n * is how records go permanently missing from the index (issue GSM-266).\n */\nexport type UpsertIndexBatchResult = {\n attempted: number\n written: number\n failedRecordIds: string[]\n searchTokenFailures: number\n}\n\nexport class QueryIndexBatchWriteError extends Error {\n readonly entityType: string\n readonly attempted: number\n readonly written: number\n readonly failedRecordIds: string[]\n\n constructor(entityType: string, result: UpsertIndexBatchResult) {\n const preview = result.failedRecordIds.slice(0, 10)\n const suffix = result.failedRecordIds.length > preview.length ? ', ...' : ''\n super(\n `[internal] Query index write incomplete for ${entityType}: wrote ${result.written} of ${result.attempted} records` +\n (preview.length ? ` (failed: ${preview.join(', ')}${suffix})` : ''),\n )\n this.name = 'QueryIndexBatchWriteError'\n this.entityType = entityType\n this.attempted = result.attempted\n this.written = result.written\n this.failedRecordIds = result.failedRecordIds\n }\n}\n\nexport function assertIndexBatchWritesLanded(entityType: string, result: UpsertIndexBatchResult): void {\n if (result.written >= result.attempted && result.failedRecordIds.length === 0) return\n throw new QueryIndexBatchWriteError(entityType, result)\n}\n\nexport function createEmptyUpsertIndexBatchResult(): UpsertIndexBatchResult {\n return { attempted: 0, written: 0, failedRecordIds: [], searchTokenFailures: 0 }\n}\n\nexport function mergeUpsertIndexBatchResults(\n target: UpsertIndexBatchResult,\n addition: UpsertIndexBatchResult,\n): UpsertIndexBatchResult {\n target.attempted += addition.attempted\n target.written += addition.written\n target.searchTokenFailures += addition.searchTokenFailures\n target.failedRecordIds.push(...addition.failedRecordIds)\n return target\n}\n\ntype CustomFieldRow = {\n record_id: string\n field_key: string\n value_text: string | null\n value_multiline: string | null\n value_int: number | null\n value_float: number | null\n value_bool: boolean | null\n organization_id: string | null\n tenant_id: string | null\n}\n\nexport type IndexBatchOptions = {\n deriveOrganizationId?: (row: AnyRow) => string | null | undefined\n encryptDoc?: (\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => Promise<Record<string, unknown> | null | undefined>\n decryptDoc?: (\n entityType: string,\n doc: Record<string, unknown>,\n scope: { organizationId: string | null; tenantId: string | null },\n ) => Promise<Record<string, unknown> | null | undefined>\n}\n\nfunction normalizeId(value: unknown): string {\n return String(value)\n}\n\nfunction normalizeScopedValue(value: unknown): string | null {\n if (value === undefined || value === null || value === '') return null\n return String(value)\n}\n\ntype IndexRowPayload = {\n entity_type: string\n entity_id: string\n organization_id: string | null\n tenant_id: string | null\n doc: Record<string, unknown>\n tokenDoc: Record<string, unknown>\n index_version: number\n}\n\nasync function updateIndexRow(db: Kysely<any>, payload: IndexRowPayload): Promise<number> {\n let updateQuery = db\n .updateTable('entity_indexes' as any)\n .set({\n doc: sql`${JSON.stringify(payload.doc)}::jsonb`,\n index_version: payload.index_version,\n organization_id: payload.organization_id ?? null,\n tenant_id: payload.tenant_id ?? null,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .where('entity_type' as any, '=', payload.entity_type)\n .where('entity_id' as any, '=', payload.entity_id)\n updateQuery = payload.organization_id == null\n ? updateQuery.where('organization_id' as any, 'is', null as any)\n : updateQuery.where('organization_id' as any, '=', payload.organization_id)\n const result = await updateQuery.executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n return Number(result?.numUpdatedRows ?? 0)\n}\n\nexport async function upsertIndexBatch(\n db: Kysely<any>,\n entityType: string,\n rows: AnyRow[],\n scope: ScopeOverrides,\n options: IndexBatchOptions = {},\n): Promise<UpsertIndexBatchResult> {\n if (!rows.length) return createEmptyUpsertIndexBatchResult()\n const recordIds = rows.map((row) => normalizeId(row.id))\n\n const failedRecordIds: string[] = []\n let searchTokenFailures = 0\n let recordedRowErrors = 0\n\n const recordRowFailure = async (\n recordId: string,\n error: unknown,\n handler: string,\n rowScope: { organizationId: string | null; tenantId: string | null },\n ): Promise<void> => {\n failedRecordIds.push(recordId)\n if (recordedRowErrors >= MAX_RECORDED_ROW_ERRORS_PER_BATCH) return\n recordedRowErrors += 1\n await recordIndexerError(\n { db },\n {\n source: 'query_index',\n handler,\n error,\n entityType,\n recordId,\n tenantId: rowScope.tenantId,\n organizationId: rowScope.organizationId,\n },\n ).catch(() => undefined)\n }\n\n const shouldMergeCustomerEntity =\n entityType === 'customers:customer_person_profile' || entityType === 'customers:customer_company_profile'\n\n let customerEntitiesById: Map<string, AnyRow> | null = null\n if (shouldMergeCustomerEntity) {\n const entityIds = Array.from(\n new Set(\n rows\n .map((row) => (row as AnyRow).entity_id || (row as AnyRow).entityId)\n .filter((value): value is string | number => value !== undefined && value !== null && `${value}` !== '')\n .map((value) => normalizeId(value)),\n ),\n )\n if (entityIds.length) {\n const entityRows = await db\n .selectFrom('customer_entities' as any)\n .selectAll()\n .where('id' as any, 'in', entityIds)\n .execute() as AnyRow[]\n customerEntitiesById = new Map(entityRows.map((row) => [normalizeId(row.id), row]))\n }\n }\n\n const customFieldRows = await db\n .selectFrom('custom_field_values' as any)\n .select([\n 'record_id' as any,\n 'field_key' as any,\n 'value_text' as any,\n 'value_multiline' as any,\n 'value_int' as any,\n 'value_float' as any,\n 'value_bool' as any,\n 'organization_id' as any,\n 'tenant_id' as any,\n ])\n .where('entity_id' as any, '=', entityType)\n .where('record_id' as any, 'in', recordIds)\n .execute() as CustomFieldRow[]\n\n const customFieldMap = new Map<string, CustomFieldRow[]>()\n for (const fieldRow of customFieldRows) {\n const key = normalizeId(fieldRow.record_id)\n const bucket = customFieldMap.get(key)\n if (bucket) bucket.push(fieldRow)\n else customFieldMap.set(key, [fieldRow])\n }\n\n const basePayloads: IndexRowPayload[] = []\n\n const debugEnabled = isSearchDebugEnabled()\n const searchConfig = resolveSearchConfig()\n\n for (const row of rows) {\n const recordId = normalizeId(row.id)\n const baseOrg = normalizeScopedValue((row as AnyRow).organization_id)\n const baseTenant = normalizeScopedValue((row as AnyRow).tenant_id)\n const derivedOrg = options?.deriveOrganizationId\n ? normalizeScopedValue(options.deriveOrganizationId(row))\n : undefined\n const scopeOrg =\n scope.orgId !== undefined\n ? scope.orgId\n : derivedOrg !== undefined\n ? derivedOrg\n : baseOrg\n const scopeTenant = scope.tenantId !== undefined ? scope.tenantId : baseTenant\n const inputRows = customFieldMap.get(recordId) ?? []\n const values: IndexCustomFieldValue[] = inputRows.map((fieldRow) => ({\n key: fieldRow.field_key,\n value:\n fieldRow.value_bool ??\n fieldRow.value_int ??\n fieldRow.value_float ??\n fieldRow.value_text ??\n fieldRow.value_multiline ??\n null,\n organizationId: normalizeScopedValue(fieldRow.organization_id),\n tenantId: normalizeScopedValue(fieldRow.tenant_id),\n }))\n const mergedRow = (() => {\n if (!shouldMergeCustomerEntity || !customerEntitiesById) return row\n const entityId = (row as AnyRow).entity_id || (row as AnyRow).entityId\n if (!entityId) return row\n const entityRow = customerEntitiesById.get(normalizeId(entityId))\n if (!entityRow) return row\n return { ...entityRow, ...row }\n })()\n let doc = buildIndexDocument(\n mergedRow,\n values,\n {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n },\n { entityType, config: searchConfig },\n )\n let tokenDoc: Record<string, unknown> = doc\n if (typeof options.encryptDoc === 'function') {\n try {\n const encrypted = await options.encryptDoc(entityType, doc, {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n })\n if (encrypted && typeof encrypted === 'object') {\n doc = encrypted\n tokenDoc = encrypted\n }\n } catch (encryptError) {\n // Falling through would write the plaintext document into entity_indexes,\n // which must stay encrypted at rest. Skip the row instead so the previously\n // encrypted row survives, and let the caller fail the run.\n await recordRowFailure(recordId, encryptError, 'query_index:reindex-batch:encrypt', {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n })\n continue\n }\n }\n if (typeof options.decryptDoc === 'function') {\n try {\n const decrypted = await options.decryptDoc(entityType, doc, {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n })\n if (decrypted && typeof decrypted === 'object') {\n // Rebuilt on the decrypted copy: `search_text` is on no encryption map, so the\n // aggregate composed at document-build time still concatenates the ciphertext\n // values the row carries at rest and tokenizes into rows nothing matches (#5625).\n // The copy keeps `doc` \u2014 the encrypted row written to `entity_indexes` \u2014 untouched.\n // A caller that passes no `decryptDoc` keeps the build-time aggregate by design:\n // without a decryption step there is no plaintext to recompose it from.\n tokenDoc = rebuildAggregateSearchField(decrypted, { entityType, config: searchConfig })\n }\n } catch (decryptError) {\n // Only affects the search tokens built below; the indexed document itself is\n // unharmed, so the row still counts as written.\n logger.warn('Failed to decrypt index document for search tokens', { entityType, recordId })\n await recordIndexerError(\n { db },\n {\n source: 'fulltext',\n handler: 'query_index:reindex-batch:decrypt',\n error: decryptError,\n entityType,\n recordId,\n tenantId: scopeTenant ?? null,\n organizationId: scopeOrg ?? null,\n },\n ).catch(() => undefined)\n }\n }\n basePayloads.push({\n entity_type: entityType,\n entity_id: recordId,\n organization_id: scopeOrg ?? null,\n tenant_id: scopeTenant ?? null,\n doc,\n tokenDoc,\n index_version: 1,\n })\n if (debugEnabled) {\n const sample = {\n display_name: (tokenDoc as any).display_name,\n first_name: (tokenDoc as any).first_name,\n last_name: (tokenDoc as any).last_name,\n brand_name: (tokenDoc as any).brand_name,\n legal_name: (tokenDoc as any).legal_name,\n }\n logger.debug('Reindex batch document', {\n entityType,\n recordId,\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\n sample,\n })\n }\n }\n\n const insertRows = basePayloads.map((payload) => ({\n entity_type: payload.entity_type,\n entity_id: payload.entity_id,\n organization_id: payload.organization_id,\n tenant_id: payload.tenant_id,\n doc: sql`${JSON.stringify(payload.doc)}::jsonb`,\n index_version: payload.index_version,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n }))\n\n const tokenPayloads = basePayloads.map((payload) => ({\n entityType: payload.entity_type,\n recordId: payload.entity_id,\n organizationId: payload.organization_id,\n tenantId: payload.tenant_id,\n doc: payload.tokenDoc,\n }))\n\n const writeSearchTokens = async (payloads = tokenPayloads): Promise<void> => {\n if (!payloads.length) return\n try {\n await replaceSearchTokensForBatch(db, payloads)\n } catch (searchTokenError) {\n // Record instead of swallowing: a failed token write leaves fulltext search stale.\n // Not counted as a write failure \u2014 replaceSearchTokensForBatch is transactional, so\n // tokens end up stale rather than missing, and the record's next write rebuilds them.\n searchTokenFailures += 1\n await recordIndexerError(\n { db },\n { source: 'fulltext', handler: 'query_index:reindex-batch', error: searchTokenError, entityType, tenantId: scope.tenantId ?? null, organizationId: scope.orgId ?? null },\n ).catch(() => undefined)\n }\n }\n\n const buildResult = (written: number): UpsertIndexBatchResult => ({\n attempted: rows.length,\n written,\n failedRecordIds,\n searchTokenFailures,\n })\n\n if (!basePayloads.length) return buildResult(0)\n\n let bulkWriteFailed = false\n try {\n // One statement, so it is atomic on its own. Note the arbiter is\n // (entity_type, entity_id, organization_id_coalesced): two payloads sharing a\n // conflict key in the same batch would raise 21000 for the whole statement and\n // fall through to the per-row path below.\n await db\n .insertInto('entity_indexes' as any)\n .values(insertRows as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'entity_id', 'organization_id_coalesced'])\n .doUpdateSet({\n doc: sql`excluded.doc`,\n index_version: sql`excluded.index_version`,\n organization_id: sql`excluded.organization_id`,\n tenant_id: sql`excluded.tenant_id`,\n deleted_at: sql`excluded.deleted_at`,\n updated_at: sql`now()`,\n } as any))\n .execute()\n } catch (bulkError) {\n bulkWriteFailed = true\n logger.warn('Bulk index upsert failed; falling back to per-row writes', {\n entityType,\n records: basePayloads.length,\n })\n await recordIndexerError(\n { db },\n {\n source: 'query_index',\n handler: 'query_index:reindex-batch:bulk',\n error: bulkError,\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.orgId ?? null,\n },\n ).catch(() => undefined)\n }\n\n if (!bulkWriteFailed) {\n await writeSearchTokens()\n if (debugEnabled) {\n logger.debug('Reindex batch tokens', {\n entityType,\n records: basePayloads.length,\n scopeOrg: scope.orgId ?? null,\n scopeTenant: scope.tenantId ?? null,\n })\n }\n return buildResult(basePayloads.length)\n }\n\n // Deliberately NOT wrapped in a transaction. In Postgres a single failing statement\n // aborts the surrounding transaction, and COMMIT on an aborted transaction answers\n // with a ROLLBACK tag instead of raising \u2014 so one bad row used to discard the entire\n // batch while this function reported success (issue GSM-266). Each row here is an\n // independent, idempotent upsert; they share no invariant worth a transaction.\n let written = 0\n for (const payload of basePayloads) {\n try {\n if (await updateIndexRow(db, payload) > 0) {\n written += 1\n continue\n }\n try {\n await db\n .insertInto('entity_indexes' as any)\n .values({\n entity_type: payload.entity_type,\n entity_id: payload.entity_id,\n organization_id: payload.organization_id,\n tenant_id: payload.tenant_id,\n doc: sql`${JSON.stringify(payload.doc)}::jsonb`,\n index_version: payload.index_version,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .execute()\n written += 1\n } catch (insertError) {\n // A concurrent worker may have inserted the row between our UPDATE and INSERT.\n // Only a successful retry of the UPDATE proves the row is actually there.\n if (isUniqueViolation(insertError) && await updateIndexRow(db, payload) > 0) {\n written += 1\n continue\n }\n await recordRowFailure(payload.entity_id, insertError, 'query_index:reindex-batch:row', {\n organizationId: payload.organization_id,\n tenantId: payload.tenant_id,\n })\n }\n } catch (rowError) {\n await recordRowFailure(payload.entity_id, rowError, 'query_index:reindex-batch:row', {\n organizationId: payload.organization_id,\n tenantId: payload.tenant_id,\n })\n }\n }\n\n const failedRecordIdSet = new Set(failedRecordIds)\n await writeSearchTokens(tokenPayloads.filter((payload) => !failedRecordIdSet.has(payload.recordId)))\n return buildResult(written)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["written"]
7
7
  }
@@ -49,6 +49,11 @@ function attachAggregateSearchField(doc, options = {}) {
49
49
  }
50
50
  return doc;
51
51
  }
52
+ function rebuildAggregateSearchField(doc, options = {}) {
53
+ const copy = { ...doc };
54
+ if (AGGREGATE_SEARCH_FIELD in copy) copy[AGGREGATE_SEARCH_FIELD] = "";
55
+ return attachAggregateSearchField(copy, options);
56
+ }
52
57
  function buildIndexDocument(baseRow, customFieldValues = [], scope = {}, options = {}) {
53
58
  const doc = {};
54
59
  for (const [key, value] of Object.entries(baseRow)) {
@@ -87,6 +92,7 @@ function buildIndexDocument(baseRow, customFieldValues = [], scope = {}, options
87
92
  export {
88
93
  AGGREGATE_SEARCH_FIELD,
89
94
  attachAggregateSearchField,
90
- buildIndexDocument
95
+ buildIndexDocument,
96
+ rebuildAggregateSearchField
91
97
  };
92
98
  //# sourceMappingURL=document.js.map