@open-mercato/core 0.6.6-develop.6413.1.ae875fb682 → 0.6.6-develop.6419.1.a632f417c4

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 (32) hide show
  1. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +5 -1
  2. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
  3. package/dist/modules/messages/api/[id]/route.js +25 -2
  4. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  5. package/dist/modules/messages/api/openapi.js +1 -0
  6. package/dist/modules/messages/api/openapi.js.map +2 -2
  7. package/dist/modules/messages/commands/actions.js +8 -0
  8. package/dist/modules/messages/commands/actions.js.map +2 -2
  9. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +50 -24
  10. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  11. package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js +1 -0
  12. package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js.map +2 -2
  13. package/dist/modules/messages/lib/constants.js +3 -1
  14. package/dist/modules/messages/lib/constants.js.map +2 -2
  15. package/dist/modules/query_index/lib/batch.js +11 -2
  16. package/dist/modules/query_index/lib/batch.js.map +2 -2
  17. package/dist/modules/query_index/lib/search-tokens.js +2 -31
  18. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  19. package/dist/modules/sync_excel/lib/adapters/customers.js +6 -6
  20. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  21. package/package.json +7 -7
  22. package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +5 -1
  23. package/src/modules/messages/api/[id]/route.ts +30 -1
  24. package/src/modules/messages/api/openapi.ts +1 -0
  25. package/src/modules/messages/commands/actions.ts +14 -0
  26. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +53 -28
  27. package/src/modules/messages/components/message-detail/panels/composer-dialogs.tsx +1 -0
  28. package/src/modules/messages/components/message-detail/types.ts +1 -0
  29. package/src/modules/messages/lib/constants.ts +4 -0
  30. package/src/modules/query_index/lib/batch.ts +15 -2
  31. package/src/modules/query_index/lib/search-tokens.ts +5 -32
  32. package/src/modules/sync_excel/lib/adapters/customers.ts +6 -6
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { flash } from "@open-mercato/ui/backend/FlashMessages";
4
+ import { apiCall, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
6
+ import { surfaceRecordConflict } from "@open-mercato/ui/backend/conflicts";
4
7
  import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
5
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
6
8
  import { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from "../utils.js";
7
9
  function toStateChangeErrorMessage(payload, t) {
8
10
  if (payload && typeof payload === "object" && !Array.isArray(payload)) {
@@ -73,20 +75,33 @@ function useMessageDetailsActions({
73
75
  setUpdatingState(true);
74
76
  try {
75
77
  await runMutation({
76
- operation: async () => {
77
- const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
78
- method: "DELETE"
79
- });
80
- if (!call.ok) {
81
- throw new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message."));
78
+ // Carry the OSS optimistic-lock header so a stale tab cannot delete a
79
+ // concurrently-changed message; throw a status-carrying error on failure
80
+ // so the 409 conflict surfaces on the shared RecordConflictBanner.
81
+ operation: () => withScopedApiRequestHeaders(
82
+ buildOptimisticLockHeader(detail?.updatedAt),
83
+ async () => {
84
+ const call = await apiCall(`/api/messages/${encodeURIComponent(id)}`, {
85
+ method: "DELETE"
86
+ });
87
+ if (!call.ok) {
88
+ throw Object.assign(
89
+ new Error(toErrorMessage(call.result) ?? t("messages.errors.deleteFailed", "Failed to delete message.")),
90
+ {
91
+ status: call.status,
92
+ ...call.result && typeof call.result === "object" ? call.result : {}
93
+ }
94
+ );
95
+ }
82
96
  }
83
- },
97
+ ),
84
98
  context: { resourceKind: "message", messageId: id, action: "delete", retryLastMutation },
85
99
  mutationPayload: { messageId: id, action: "delete" }
86
100
  });
87
101
  flash(t("messages.flash.deleted", "Message deleted."), "success");
88
102
  onDeleted();
89
103
  } catch (err) {
104
+ if (surfaceRecordConflict(err, t)) return;
90
105
  flash(
91
106
  err instanceof Error ? err.message : t("messages.errors.deleteFailed", "Failed to delete message."),
92
107
  "error"
@@ -94,7 +109,7 @@ function useMessageDetailsActions({
94
109
  } finally {
95
110
  setUpdatingState(false);
96
111
  }
97
- }, [id, onDeleted, retryLastMutation, runMutation, t]);
112
+ }, [detail?.updatedAt, id, onDeleted, retryLastMutation, runMutation, t]);
98
113
  const runConversationAction = React.useCallback(async (action, options) => {
99
114
  setActiveConversationAction(action);
100
115
  try {
@@ -187,22 +202,32 @@ function useMessageDetailsActions({
187
202
  setExecutingActionId(action.id);
188
203
  try {
189
204
  const call = await runMutation({
190
- operation: async () => {
191
- const result = await apiCall(
192
- `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
193
- {
194
- method: "POST",
195
- headers: { "Content-Type": "application/json" },
196
- body: JSON.stringify(payload ?? {})
197
- }
198
- );
199
- if (!result.ok) {
200
- throw new Error(
201
- toErrorMessage(result.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")
205
+ // Carry the OSS optimistic-lock header so a stale tab cannot execute an
206
+ // action against a concurrently-changed message; throw a status-carrying
207
+ // error on failure so the 409 conflict surfaces on the shared banner.
208
+ operation: () => withScopedApiRequestHeaders(
209
+ buildOptimisticLockHeader(detail?.updatedAt),
210
+ async () => {
211
+ const result = await apiCall(
212
+ `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
213
+ {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify(payload ?? {})
217
+ }
202
218
  );
219
+ if (!result.ok) {
220
+ throw Object.assign(
221
+ new Error(toErrorMessage(result.result) ?? t("messages.errors.actionFailed", "Failed to execute action.")),
222
+ {
223
+ status: result.status,
224
+ ...result.result && typeof result.result === "object" ? result.result : {}
225
+ }
226
+ );
227
+ }
228
+ return result;
203
229
  }
204
- return result;
205
- },
230
+ ),
206
231
  context: { resourceKind: "message", messageId: id, action: "execute-action", retryLastMutation },
207
232
  mutationPayload: { messageId: id, action: "execute-action", actionId: action.id }
208
233
  });
@@ -214,6 +239,7 @@ function useMessageDetailsActions({
214
239
  }
215
240
  await detailQuery.refetch();
216
241
  } catch (err) {
242
+ if (surfaceRecordConflict(err, t)) return;
217
243
  flash(
218
244
  err instanceof Error ? err.message : t("messages.errors.actionFailed", "Failed to execute action."),
219
245
  "error"
@@ -221,7 +247,7 @@ function useMessageDetailsActions({
221
247
  } finally {
222
248
  setExecutingActionId(null);
223
249
  }
224
- }, [detailQuery, id, retryLastMutation, runMutation, t]);
250
+ }, [detail?.updatedAt, detailQuery, id, retryLastMutation, runMutation, t]);
225
251
  const handleExecuteAction = React.useCallback(async (action, payload) => {
226
252
  if (executingActionId || detail?.actionTaken) return;
227
253
  if (action.confirmRequired) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { UseQueryResult } from '@tanstack/react-query'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport type {\n ActionResult,\n MessageAction,\n MessageAttachment,\n MessageDetail,\n PendingActionConfirmation,\n} from '../types'\nimport { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../utils'\n\ntype RequestAndRefreshOptions = {\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n}\n\ntype ConversationActionKind =\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'deleteConversation'\n | 'markAllUnread'\n | 'markAllRead'\n\ntype MessageDetailMutationContext = {\n resourceKind: 'message' | 'conversation'\n messageId: string\n action: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessageDetailsActionsInput = {\n id: string\n t: TranslateFn\n detail: MessageDetail | null\n detailQuery: UseQueryResult<MessageDetail | null, Error>\n attachments: MessageAttachment[] | undefined\n isArchived: boolean\n onDeleted: () => void\n onMarkedUnread: () => void\n refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n suppressAutoMarkRead: () => void\n}\n\nfunction toStateChangeErrorMessage(payload: unknown, t: TranslateFn): string | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n const code = (payload as Record<string, unknown>).code\n if (code === 'messages_sender_archive_unsupported') {\n return t(\n 'messages.errors.archiveSenderMessageUnsupported',\n 'You cannot archive messages you sent.',\n )\n }\n }\n return toErrorMessage(payload)\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n onMarkedUnread,\n refreshDetailWithoutAutoMarkRead,\n suppressAutoMarkRead,\n}: UseMessageDetailsActionsInput) {\n const [replyOpen, setReplyOpen] = React.useState(false)\n const [forwardOpen, setForwardOpen] = React.useState(false)\n const [editOpen, setEditOpen] = React.useState(false)\n const [updatingState, setUpdatingState] = React.useState(false)\n const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)\n const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState<PendingActionConfirmation | null>(null)\n const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)\n const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({\n contextId: 'messages-detail-actions',\n })\n\n const requestAndRefresh = React.useCallback(async (\n url: string,\n method: 'PUT' | 'DELETE',\n options?: RequestAndRefreshOptions,\n ) => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(url, { method })\n if (!call.ok) {\n throw new Error(\n toStateChangeErrorMessage(call.result, t)\n ?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n )\n }\n if (options?.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },\n mutationPayload: { messageId: id, action: 'state-change', url, method },\n })\n options?.onSuccess?.()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const handleDelete = React.useCallback(async () => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n })\n if (!call.ok) {\n throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },\n mutationPayload: { messageId: id, action: 'delete' },\n })\n flash(t('messages.flash.deleted', 'Message deleted.'), 'success')\n onDeleted()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.deleteFailed', 'Failed to delete message.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [id, onDeleted, retryLastMutation, runMutation, t])\n\n const runConversationAction = React.useCallback(async (\n action: ConversationActionKind,\n options: {\n url: string\n method: 'PUT' | 'DELETE'\n successMessage: string\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n },\n ) => {\n setActiveConversationAction(action)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n )\n }\n },\n context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },\n mutationPayload: { messageId: id, action, url: options.url, method: options.method },\n })\n\n flash(options.successMessage, 'success')\n\n if (options.onSuccess) {\n options.onSuccess()\n return\n }\n\n if (options.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n 'error',\n )\n } finally {\n setActiveConversationAction(null)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const archiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('archiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationArchived', 'Conversation archived.'),\n })\n }, [id, runConversationAction, t])\n\n const unarchiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('unarchiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),\n })\n }, [id, runConversationAction, t])\n\n const markConversationUnread = React.useCallback(async (messageId?: string) => {\n suppressAutoMarkRead()\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllUnread', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),\n onSuccess: onMarkedUnread,\n })\n }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])\n\n const markConversationRead = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllRead', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),\n })\n }, [id, runConversationAction, t])\n\n const deleteConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('deleteConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationDeleted', 'Conversation deleted.'),\n onSuccess: onDeleted,\n })\n }, [id, onDeleted, runConversationAction, t])\n\n const handleDeleteDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleDelete()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setDeleteConfirmationOpen(false)\n }\n }, [handleDelete])\n\n const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n setExecutingActionId(action.id)\n try {\n const call = await runMutation({\n operation: async () => {\n const result = await apiCall<ActionResult>(\n `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload ?? {}),\n },\n )\n\n if (!result.ok) {\n throw new Error(\n toErrorMessage(result.result)\n ?? t('messages.errors.actionFailed', 'Failed to execute action.'),\n )\n }\n\n return result\n },\n context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },\n mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },\n })\n\n flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')\n\n const redirectTarget = call.result?.result?.redirect\n if (\n typeof redirectTarget === 'string'\n && redirectTarget.trim().length > 0\n && isSafeNavigationHref(redirectTarget)\n ) {\n window.location.href = redirectTarget\n return\n }\n\n await detailQuery.refetch()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.actionFailed', 'Failed to execute action.'),\n 'error',\n )\n } finally {\n setExecutingActionId(null)\n }\n }, [detailQuery, id, retryLastMutation, runMutation, t])\n\n const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n if (executingActionId || detail?.actionTaken) return\n\n if (action.confirmRequired) {\n setPendingActionConfirmation({ action, payload })\n return\n }\n\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId])\n\n const handleConfirmPendingAction = React.useCallback(async () => {\n if (!pendingActionConfirmation || executingActionId || detail?.actionTaken) return\n const { action, payload } = pendingActionConfirmation\n setPendingActionConfirmation(null)\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId, pendingActionConfirmation])\n\n const handleActionConfirmDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleConfirmPendingAction()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setPendingActionConfirmation(null)\n }\n }, [handleConfirmPendingAction])\n\n const contentProps = React.useMemo<MessageContentProps>(() => ({\n message: {\n id: detail?.id ?? '',\n type: detail?.type ?? 'default',\n subject: detail?.subject ?? '',\n body: detail?.body ?? '',\n bodyFormat: detail?.bodyFormat ?? 'text',\n priority: (detail?.priority ?? 'normal') as 'low' | 'normal' | 'high' | 'urgent',\n sentAt: detail?.sentAt ? new Date(detail.sentAt) : null,\n senderName: detail?.senderName ?? undefined,\n senderUserId: detail?.senderUserId ?? '',\n actionData: detail?.actionData ?? null,\n actionTaken: detail?.actionTaken ?? null,\n actionTakenAt: detail?.actionTakenAt ? new Date(detail.actionTakenAt) : null,\n },\n objects: (detail?.objects ?? []).map((item) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n snapshot: item.snapshot ?? undefined,\n })),\n attachments: (attachments ?? []).map((item) => ({\n id: item.id,\n fileName: item.fileName,\n fileSize: item.fileSize,\n mimeType: item.mimeType,\n url: item.url,\n })),\n }), [attachments, detail])\n\n const handleExecuteActionById = React.useCallback<MessageActionsProps['onExecuteAction']>(async (actionId, payload) => {\n const action = detail?.actionData?.actions?.find((entry) => {\n if (parseObjectActionId(entry.id)) return false\n return entry.id === actionId\n })\n if (!action) return\n await handleExecuteAction(action, payload)\n }, [detail?.actionData?.actions, handleExecuteAction])\n\n const messageActions = React.useMemo(() => {\n return (detail?.actionData?.actions ?? []).filter((action) => !parseObjectActionId(action.id))\n }, [detail?.actionData?.actions])\n\n const objectActionsByObjectId = React.useMemo(() => {\n const byObjectId = new Map<string, Map<string, MessageAction>>()\n for (const action of detail?.actionData?.actions ?? []) {\n const parsed = parseObjectActionId(action.id)\n if (!parsed) continue\n const current = byObjectId.get(parsed.objectId) ?? new Map<string, MessageAction>()\n current.set(parsed.actionId, action)\n byObjectId.set(parsed.objectId, current)\n }\n return byObjectId\n }, [detail?.actionData?.actions])\n\n const toggleRead = React.useCallback(async () => {\n if (detail?.isRead) {\n suppressAutoMarkRead()\n }\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,\n )\n }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])\n\n const toggleArchive = React.useCallback(async () => {\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/archive`,\n isArchived ? 'DELETE' : 'PUT',\n )\n }, [id, isArchived, requestAndRefresh])\n\n return {\n replyOpen,\n setReplyOpen,\n forwardOpen,\n setForwardOpen,\n editOpen,\n setEditOpen,\n updatingState,\n executingActionId,\n pendingActionConfirmation,\n setPendingActionConfirmation,\n deleteConfirmationOpen,\n setDeleteConfirmationOpen,\n activeConversationAction,\n toggleRead,\n toggleArchive,\n archiveConversation,\n unarchiveConversation,\n deleteConversation,\n markConversationUnread,\n markConversationRead,\n requestAndRefresh,\n handleDelete,\n handleDeleteDialogKeyDown,\n handleExecuteAction,\n handleConfirmPendingAction,\n handleActionConfirmDialogKeyDown,\n contentProps,\n handleExecuteActionById,\n messageActions,\n objectActionsByObjectId,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { UseQueryResult } from '@tanstack/react-query'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport type {\n ActionResult,\n MessageAction,\n MessageAttachment,\n MessageDetail,\n PendingActionConfirmation,\n} from '../types'\nimport { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../utils'\n\ntype RequestAndRefreshOptions = {\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n}\n\ntype ConversationActionKind =\n | 'archiveConversation'\n | 'unarchiveConversation'\n | 'deleteConversation'\n | 'markAllUnread'\n | 'markAllRead'\n\ntype MessageDetailMutationContext = {\n resourceKind: 'message' | 'conversation'\n messageId: string\n action: string\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessageDetailsActionsInput = {\n id: string\n t: TranslateFn\n detail: MessageDetail | null\n detailQuery: UseQueryResult<MessageDetail | null, Error>\n attachments: MessageAttachment[] | undefined\n isArchived: boolean\n onDeleted: () => void\n onMarkedUnread: () => void\n refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>\n suppressAutoMarkRead: () => void\n}\n\nfunction toStateChangeErrorMessage(payload: unknown, t: TranslateFn): string | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n const code = (payload as Record<string, unknown>).code\n if (code === 'messages_sender_archive_unsupported') {\n return t(\n 'messages.errors.archiveSenderMessageUnsupported',\n 'You cannot archive messages you sent.',\n )\n }\n }\n return toErrorMessage(payload)\n}\n\nexport function useMessageDetailsActions({\n id,\n t,\n detail,\n detailQuery,\n attachments,\n isArchived,\n onDeleted,\n onMarkedUnread,\n refreshDetailWithoutAutoMarkRead,\n suppressAutoMarkRead,\n}: UseMessageDetailsActionsInput) {\n const [replyOpen, setReplyOpen] = React.useState(false)\n const [forwardOpen, setForwardOpen] = React.useState(false)\n const [editOpen, setEditOpen] = React.useState(false)\n const [updatingState, setUpdatingState] = React.useState(false)\n const [executingActionId, setExecutingActionId] = React.useState<string | null>(null)\n const [pendingActionConfirmation, setPendingActionConfirmation] = React.useState<PendingActionConfirmation | null>(null)\n const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)\n const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)\n\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({\n contextId: 'messages-detail-actions',\n })\n\n const requestAndRefresh = React.useCallback(async (\n url: string,\n method: 'PUT' | 'DELETE',\n options?: RequestAndRefreshOptions,\n ) => {\n setUpdatingState(true)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean }>(url, { method })\n if (!call.ok) {\n throw new Error(\n toStateChangeErrorMessage(call.result, t)\n ?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n )\n }\n if (options?.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n },\n context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },\n mutationPayload: { messageId: id, action: 'state-change', url, method },\n })\n options?.onSuccess?.()\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.stateChangeFailed', 'Failed to update message state.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const handleDelete = React.useCallback(async () => {\n setUpdatingState(true)\n try {\n await runMutation({\n // Carry the OSS optimistic-lock header so a stale tab cannot delete a\n // concurrently-changed message; throw a status-carrying error on failure\n // so the 409 conflict surfaces on the shared RecordConflictBanner.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(detail?.updatedAt),\n async () => {\n const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n })\n if (!call.ok) {\n throw Object.assign(\n new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.')),\n {\n status: call.status,\n ...(call.result && typeof call.result === 'object' ? call.result : {}),\n },\n )\n }\n },\n ),\n context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },\n mutationPayload: { messageId: id, action: 'delete' },\n })\n flash(t('messages.flash.deleted', 'Message deleted.'), 'success')\n onDeleted()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.deleteFailed', 'Failed to delete message.'),\n 'error',\n )\n } finally {\n setUpdatingState(false)\n }\n }, [detail?.updatedAt, id, onDeleted, retryLastMutation, runMutation, t])\n\n const runConversationAction = React.useCallback(async (\n action: ConversationActionKind,\n options: {\n url: string\n method: 'PUT' | 'DELETE'\n successMessage: string\n skipDetailAutoMarkRead?: boolean\n onSuccess?: () => void\n },\n ) => {\n setActiveConversationAction(action)\n try {\n await runMutation({\n operation: async () => {\n const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n )\n }\n },\n context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },\n mutationPayload: { messageId: id, action, url: options.url, method: options.method },\n })\n\n flash(options.successMessage, 'success')\n\n if (options.onSuccess) {\n options.onSuccess()\n return\n }\n\n if (options.skipDetailAutoMarkRead) {\n await refreshDetailWithoutAutoMarkRead()\n } else {\n await detailQuery.refetch()\n }\n } catch (err) {\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),\n 'error',\n )\n } finally {\n setActiveConversationAction(null)\n }\n }, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])\n\n const archiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('archiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationArchived', 'Conversation archived.'),\n })\n }, [id, runConversationAction, t])\n\n const unarchiveConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('unarchiveConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/archive`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationUnarchived', 'Conversation restored from archive.'),\n })\n }, [id, runConversationAction, t])\n\n const markConversationUnread = React.useCallback(async (messageId?: string) => {\n suppressAutoMarkRead()\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllUnread', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),\n onSuccess: onMarkedUnread,\n })\n }, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])\n\n const markConversationRead = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('markAllRead', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,\n method: 'PUT',\n successMessage: t('messages.flash.conversationMarkedRead', 'Conversation marked read.'),\n })\n }, [id, runConversationAction, t])\n\n const deleteConversation = React.useCallback(async (messageId?: string) => {\n const targetMessageId = messageId ?? id\n await runConversationAction('deleteConversation', {\n url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation`,\n method: 'DELETE',\n successMessage: t('messages.flash.conversationDeleted', 'Conversation deleted.'),\n onSuccess: onDeleted,\n })\n }, [id, onDeleted, runConversationAction, t])\n\n const handleDeleteDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleDelete()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setDeleteConfirmationOpen(false)\n }\n }, [handleDelete])\n\n const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n setExecutingActionId(action.id)\n try {\n const call = await runMutation({\n // Carry the OSS optimistic-lock header so a stale tab cannot execute an\n // action against a concurrently-changed message; throw a status-carrying\n // error on failure so the 409 conflict surfaces on the shared banner.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(detail?.updatedAt),\n async () => {\n const result = await apiCall<ActionResult>(\n `/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload ?? {}),\n },\n )\n\n if (!result.ok) {\n throw Object.assign(\n new Error(toErrorMessage(result.result) ?? t('messages.errors.actionFailed', 'Failed to execute action.')),\n {\n status: result.status,\n ...(result.result && typeof result.result === 'object' ? result.result : {}),\n },\n )\n }\n\n return result\n },\n ),\n context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },\n mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },\n })\n\n flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')\n\n const redirectTarget = call.result?.result?.redirect\n if (\n typeof redirectTarget === 'string'\n && redirectTarget.trim().length > 0\n && isSafeNavigationHref(redirectTarget)\n ) {\n window.location.href = redirectTarget\n return\n }\n\n await detailQuery.refetch()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) return\n flash(\n err instanceof Error\n ? err.message\n : t('messages.errors.actionFailed', 'Failed to execute action.'),\n 'error',\n )\n } finally {\n setExecutingActionId(null)\n }\n }, [detail?.updatedAt, detailQuery, id, retryLastMutation, runMutation, t])\n\n const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {\n if (executingActionId || detail?.actionTaken) return\n\n if (action.confirmRequired) {\n setPendingActionConfirmation({ action, payload })\n return\n }\n\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId])\n\n const handleConfirmPendingAction = React.useCallback(async () => {\n if (!pendingActionConfirmation || executingActionId || detail?.actionTaken) return\n const { action, payload } = pendingActionConfirmation\n setPendingActionConfirmation(null)\n await executeAction(action, payload)\n }, [detail?.actionTaken, executeAction, executingActionId, pendingActionConfirmation])\n\n const handleActionConfirmDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleConfirmPendingAction()\n return\n }\n\n if (event.key === 'Escape') {\n event.preventDefault()\n setPendingActionConfirmation(null)\n }\n }, [handleConfirmPendingAction])\n\n const contentProps = React.useMemo<MessageContentProps>(() => ({\n message: {\n id: detail?.id ?? '',\n type: detail?.type ?? 'default',\n subject: detail?.subject ?? '',\n body: detail?.body ?? '',\n bodyFormat: detail?.bodyFormat ?? 'text',\n priority: (detail?.priority ?? 'normal') as 'low' | 'normal' | 'high' | 'urgent',\n sentAt: detail?.sentAt ? new Date(detail.sentAt) : null,\n senderName: detail?.senderName ?? undefined,\n senderUserId: detail?.senderUserId ?? '',\n actionData: detail?.actionData ?? null,\n actionTaken: detail?.actionTaken ?? null,\n actionTakenAt: detail?.actionTakenAt ? new Date(detail.actionTakenAt) : null,\n },\n objects: (detail?.objects ?? []).map((item) => ({\n id: item.id,\n entityModule: item.entityModule,\n entityType: item.entityType,\n entityId: item.entityId,\n actionRequired: item.actionRequired,\n snapshot: item.snapshot ?? undefined,\n })),\n attachments: (attachments ?? []).map((item) => ({\n id: item.id,\n fileName: item.fileName,\n fileSize: item.fileSize,\n mimeType: item.mimeType,\n url: item.url,\n })),\n }), [attachments, detail])\n\n const handleExecuteActionById = React.useCallback<MessageActionsProps['onExecuteAction']>(async (actionId, payload) => {\n const action = detail?.actionData?.actions?.find((entry) => {\n if (parseObjectActionId(entry.id)) return false\n return entry.id === actionId\n })\n if (!action) return\n await handleExecuteAction(action, payload)\n }, [detail?.actionData?.actions, handleExecuteAction])\n\n const messageActions = React.useMemo(() => {\n return (detail?.actionData?.actions ?? []).filter((action) => !parseObjectActionId(action.id))\n }, [detail?.actionData?.actions])\n\n const objectActionsByObjectId = React.useMemo(() => {\n const byObjectId = new Map<string, Map<string, MessageAction>>()\n for (const action of detail?.actionData?.actions ?? []) {\n const parsed = parseObjectActionId(action.id)\n if (!parsed) continue\n const current = byObjectId.get(parsed.objectId) ?? new Map<string, MessageAction>()\n current.set(parsed.actionId, action)\n byObjectId.set(parsed.objectId, current)\n }\n return byObjectId\n }, [detail?.actionData?.actions])\n\n const toggleRead = React.useCallback(async () => {\n if (detail?.isRead) {\n suppressAutoMarkRead()\n }\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/read`,\n detail?.isRead ? 'DELETE' : 'PUT',\n detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,\n )\n }, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])\n\n const toggleArchive = React.useCallback(async () => {\n await requestAndRefresh(\n `/api/messages/${encodeURIComponent(id)}/archive`,\n isArchived ? 'DELETE' : 'PUT',\n )\n }, [id, isArchived, requestAndRefresh])\n\n return {\n replyOpen,\n setReplyOpen,\n forwardOpen,\n setForwardOpen,\n editOpen,\n setEditOpen,\n updatingState,\n executingActionId,\n pendingActionConfirmation,\n setPendingActionConfirmation,\n deleteConfirmationOpen,\n setDeleteConfirmationOpen,\n activeConversationAction,\n toggleRead,\n toggleArchive,\n archiveConversation,\n unarchiveConversation,\n deleteConversation,\n markConversationUnread,\n markConversationRead,\n requestAndRefresh,\n handleDelete,\n handleDeleteDialogKeyDown,\n handleExecuteAction,\n handleConfirmPendingAction,\n handleActionConfirmDialogKeyDown,\n contentProps,\n handleExecuteActionById,\n messageActions,\n objectActionsByObjectId,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,7 @@ function MessageDetailComposerDialogs(props) {
7
7
  {
8
8
  variant: "compose",
9
9
  messageId: props.id,
10
+ expectedUpdatedAt: props.detail.updatedAt,
10
11
  open: props.editOpen && props.detail.isDraft && props.detail.canEditDraft,
11
12
  onOpenChange: props.setEditOpen,
12
13
  defaultValues: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/messages/components/message-detail/panels/composer-dialogs.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport type { MessageAttachment, MessageDetail } from '../types'\n\ntype ComposerDialogsProps = {\n id: string\n detail: MessageDetail\n attachments: MessageAttachment[] | undefined\n editOpen: boolean\n setEditOpen: (value: boolean) => void\n onRefresh: () => Promise<unknown>\n}\n\nexport function MessageDetailComposerDialogs(props: ComposerDialogsProps) {\n return (\n <MessageComposer\n variant=\"compose\"\n messageId={props.id}\n open={props.editOpen && props.detail.isDraft && props.detail.canEditDraft}\n onOpenChange={props.setEditOpen}\n defaultValues={{\n type: props.detail.type,\n recipients: props.detail.recipients.map((recipient) => recipient.userId),\n subject: props.detail.subject,\n body: props.detail.body,\n bodyFormat: props.detail.bodyFormat,\n priority: props.detail.priority as 'low' | 'normal' | 'high' | 'urgent',\n visibility: props.detail.visibility ?? 'internal',\n sourceEntityType: props.detail.sourceEntityType ?? null,\n sourceEntityId: props.detail.sourceEntityId ?? null,\n externalEmail: props.detail.externalEmail ?? null,\n externalName: props.detail.externalName ?? null,\n attachmentIds: props.attachments?.map((attachment) => attachment.id) ?? [],\n }}\n onSuccess={() => {\n props.setEditOpen(false)\n void props.onRefresh()\n }}\n />\n )\n}\n"],
5
- "mappings": ";AAgBI;AAdJ,SAAS,uBAAuB;AAYzB,SAAS,6BAA6B,OAA6B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,OAAO;AAAA,MAC7D,cAAc,MAAM;AAAA,MACpB,eAAe;AAAA,QACb,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,MAAM;AAAA,QACvE,SAAS,MAAM,OAAO;AAAA,QACtB,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO;AAAA,QACzB,UAAU,MAAM,OAAO;AAAA,QACvB,YAAY,MAAM,OAAO,cAAc;AAAA,QACvC,kBAAkB,MAAM,OAAO,oBAAoB;AAAA,QACnD,gBAAgB,MAAM,OAAO,kBAAkB;AAAA,QAC/C,eAAe,MAAM,OAAO,iBAAiB;AAAA,QAC7C,cAAc,MAAM,OAAO,gBAAgB;AAAA,QAC3C,eAAe,MAAM,aAAa,IAAI,CAAC,eAAe,WAAW,EAAE,KAAK,CAAC;AAAA,MAC3E;AAAA,MACA,WAAW,MAAM;AACf,cAAM,YAAY,KAAK;AACvB,aAAK,MAAM,UAAU;AAAA,MACvB;AAAA;AAAA,EACF;AAEJ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport { MessageComposer } from '@open-mercato/ui/backend/messages'\nimport type { MessageAttachment, MessageDetail } from '../types'\n\ntype ComposerDialogsProps = {\n id: string\n detail: MessageDetail\n attachments: MessageAttachment[] | undefined\n editOpen: boolean\n setEditOpen: (value: boolean) => void\n onRefresh: () => Promise<unknown>\n}\n\nexport function MessageDetailComposerDialogs(props: ComposerDialogsProps) {\n return (\n <MessageComposer\n variant=\"compose\"\n messageId={props.id}\n expectedUpdatedAt={props.detail.updatedAt}\n open={props.editOpen && props.detail.isDraft && props.detail.canEditDraft}\n onOpenChange={props.setEditOpen}\n defaultValues={{\n type: props.detail.type,\n recipients: props.detail.recipients.map((recipient) => recipient.userId),\n subject: props.detail.subject,\n body: props.detail.body,\n bodyFormat: props.detail.bodyFormat,\n priority: props.detail.priority as 'low' | 'normal' | 'high' | 'urgent',\n visibility: props.detail.visibility ?? 'internal',\n sourceEntityType: props.detail.sourceEntityType ?? null,\n sourceEntityId: props.detail.sourceEntityId ?? null,\n externalEmail: props.detail.externalEmail ?? null,\n externalName: props.detail.externalName ?? null,\n attachmentIds: props.attachments?.map((attachment) => attachment.id) ?? [],\n }}\n onSuccess={() => {\n props.setEditOpen(false)\n void props.onRefresh()\n }}\n />\n )\n}\n"],
5
+ "mappings": ";AAgBI;AAdJ,SAAS,uBAAuB;AAYzB,SAAS,6BAA6B,OAA6B;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,mBAAmB,MAAM,OAAO;AAAA,MAChC,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,OAAO;AAAA,MAC7D,cAAc,MAAM;AAAA,MACpB,eAAe;AAAA,QACb,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO,WAAW,IAAI,CAAC,cAAc,UAAU,MAAM;AAAA,QACvE,SAAS,MAAM,OAAO;AAAA,QACtB,MAAM,MAAM,OAAO;AAAA,QACnB,YAAY,MAAM,OAAO;AAAA,QACzB,UAAU,MAAM,OAAO;AAAA,QACvB,YAAY,MAAM,OAAO,cAAc;AAAA,QACvC,kBAAkB,MAAM,OAAO,oBAAoB;AAAA,QACnD,gBAAgB,MAAM,OAAO,kBAAkB;AAAA,QAC/C,eAAe,MAAM,OAAO,iBAAiB;AAAA,QAC7C,cAAc,MAAM,OAAO,gBAAgB;AAAA,QAC3C,eAAe,MAAM,aAAa,IAAI,CAAC,eAAe,WAAW,EAAE,KAAK,CAAC;AAAA,MAC3E;AAAA,MACA,WAAW,MAAM;AACf,cAAM,YAAY,KAAK;AACvB,aAAK,MAAM,UAAU;AAAA,MACvB;AAAA;AAAA,EACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,11 @@
1
1
  const MESSAGE_ENTITY_ID = "messages:message";
2
2
  const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID;
3
3
  const MESSAGE_ATTACHMENT_PARTITION = "messages";
4
+ const MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND = "messages.message";
4
5
  export {
5
6
  MESSAGE_ATTACHMENT_ENTITY_ID,
6
7
  MESSAGE_ATTACHMENT_PARTITION,
7
- MESSAGE_ENTITY_ID
8
+ MESSAGE_ENTITY_ID,
9
+ MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND
8
10
  };
9
11
  //# sourceMappingURL=constants.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/lib/constants.ts"],
4
- "sourcesContent": ["export const MESSAGE_ENTITY_ID = 'messages:message'\nexport const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID\nexport const MESSAGE_ATTACHMENT_PARTITION = 'messages'\n"],
5
- "mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;",
4
+ "sourcesContent": ["export const MESSAGE_ENTITY_ID = 'messages:message'\nexport const MESSAGE_ATTACHMENT_ENTITY_ID = MESSAGE_ENTITY_ID\nexport const MESSAGE_ATTACHMENT_PARTITION = 'messages'\n\n// Resource kind used by the command-level OSS optimistic-lock guard so stale\n// draft edits and message actions fail with the structured 409 conflict.\nexport const MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND = 'messages.message'\n"],
5
+ "mappings": "AAAO,MAAM,oBAAoB;AAC1B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;AAIrC,MAAM,wCAAwC;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { sql } from "kysely";
2
+ import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
2
3
  import { buildIndexDocument } from "./document.js";
3
4
  import { replaceSearchTokensForBatch, isSearchDebugEnabled } from "./search-tokens.js";
4
5
  function normalizeId(value) {
@@ -151,7 +152,11 @@ async function upsertIndexBatch(db, entityType, rows, scope, options = {}) {
151
152
  })).execute();
152
153
  try {
153
154
  await replaceSearchTokensForBatch(db, tokenPayloads);
154
- } catch {
155
+ } catch (searchTokenError) {
156
+ await recordIndexerError(
157
+ { db },
158
+ { source: "fulltext", handler: "query_index:reindex-batch", error: searchTokenError, entityType, tenantId: scope.tenantId ?? null, organizationId: scope.orgId ?? null }
159
+ ).catch(() => void 0);
155
160
  }
156
161
  if (debugEnabled) {
157
162
  console.info("[reindex:batch:tokens]", {
@@ -195,7 +200,11 @@ async function upsertIndexBatch(db, entityType, rows, scope, options = {}) {
195
200
  }
196
201
  try {
197
202
  await replaceSearchTokensForBatch(db, tokenPayloads);
198
- } catch {
203
+ } catch (searchTokenError) {
204
+ await recordIndexerError(
205
+ { db },
206
+ { source: "fulltext", handler: "query_index:reindex-batch", error: searchTokenError, entityType, tenantId: scope.tenantId ?? null, organizationId: scope.orgId ?? null }
207
+ ).catch(() => void 0);
199
208
  }
200
209
  }
201
210
  export {
@@ -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 { buildIndexDocument, type IndexCustomFieldValue } from './document'\nimport { replaceSearchTokensForBatch, isSearchDebugEnabled } from './search-tokens'\n\nexport type AnyRow = Record<string, any> & { id: string | number }\n\nexport type ScopeOverrides = {\n orgId?: string\n tenantId?: string\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\nexport async function upsertIndexBatch(\n db: Kysely<any>,\n entityType: string,\n rows: AnyRow[],\n scope: ScopeOverrides,\n options: IndexBatchOptions = {},\n): Promise<void> {\n if (!rows.length) return\n const recordIds = rows.map((row) => normalizeId(row.id))\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: Array<{\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\n const debugEnabled = isSearchDebugEnabled()\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(mergedRow, values, {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\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 {\n // best-effort; ignore encrypt errors during indexing\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 {\n // best-effort; ignore decrypt errors during indexing\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 console.info('[reindex:batch:doc]', {\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 try {\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 try {\n await replaceSearchTokensForBatch(db, tokenPayloads)\n } catch {}\n if (debugEnabled) {\n console.info('[reindex:batch:tokens]', {\n entityType,\n records: basePayloads.length,\n scopeOrg: scope.orgId ?? null,\n scopeTenant: scope.tenantId ?? null,\n })\n }\n return\n } catch {\n await db.transaction().execute(async (trx) => {\n for (const payload of basePayloads) {\n let updateQuery = trx\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 if (result && Number(result.numUpdatedRows ?? 0) > 0) continue\n try {\n await trx\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 } catch {\n // ignore duplicate insert race; another concurrent worker updated the row\n }\n }\n })\n }\n try {\n await replaceSearchTokensForBatch(db, tokenPayloads)\n } catch {}\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 { buildIndexDocument, type IndexCustomFieldValue } from './document'\nimport { replaceSearchTokensForBatch, isSearchDebugEnabled } from './search-tokens'\n\nexport type AnyRow = Record<string, any> & { id: string | number }\n\nexport type ScopeOverrides = {\n orgId?: string\n tenantId?: string\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\nexport async function upsertIndexBatch(\n db: Kysely<any>,\n entityType: string,\n rows: AnyRow[],\n scope: ScopeOverrides,\n options: IndexBatchOptions = {},\n): Promise<void> {\n if (!rows.length) return\n const recordIds = rows.map((row) => normalizeId(row.id))\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: Array<{\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\n const debugEnabled = isSearchDebugEnabled()\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(mergedRow, values, {\n organizationId: scopeOrg ?? null,\n tenantId: scopeTenant ?? null,\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 {\n // best-effort; ignore encrypt errors during indexing\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 {\n // best-effort; ignore decrypt errors during indexing\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 console.info('[reindex:batch:doc]', {\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 try {\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 try {\n await replaceSearchTokensForBatch(db, tokenPayloads)\n } catch (searchTokenError) {\n // Record instead of swallowing: a failed token write leaves fulltext search stale.\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 if (debugEnabled) {\n console.info('[reindex:batch:tokens]', {\n entityType,\n records: basePayloads.length,\n scopeOrg: scope.orgId ?? null,\n scopeTenant: scope.tenantId ?? null,\n })\n }\n return\n } catch {\n await db.transaction().execute(async (trx) => {\n for (const payload of basePayloads) {\n let updateQuery = trx\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 if (result && Number(result.numUpdatedRows ?? 0) > 0) continue\n try {\n await trx\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 } catch {\n // ignore duplicate insert race; another concurrent worker updated the row\n }\n }\n })\n }\n try {\n await replaceSearchTokensForBatch(db, tokenPayloads)\n } catch (searchTokenError) {\n // Record instead of swallowing: a failed token write leaves fulltext search stale.\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"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -150,8 +150,6 @@ async function replaceSearchTokensForBatch(db, payloads) {
150
150
  }
151
151
  const scopeKey = (org, tenant) => `${org ?? "__null__"}|${tenant ?? "__null__"}`;
152
152
  const scopeBuckets = /* @__PURE__ */ new Map();
153
- const fieldPairsByScope = /* @__PURE__ */ new Map();
154
- const seenPairsByScope = /* @__PURE__ */ new Map();
155
153
  for (const payload of payloads) {
156
154
  const org = payload.organizationId ?? null;
157
155
  const tenant = payload.tenantId ?? null;
@@ -160,36 +158,9 @@ async function replaceSearchTokensForBatch(db, payloads) {
160
158
  bucket.ids.add(String(payload.recordId));
161
159
  scopeBuckets.set(key, bucket);
162
160
  }
163
- for (const payload of payloads) {
164
- const org = payload.organizationId ?? null;
165
- const tenant = payload.tenantId ?? null;
166
- const key = scopeKey(org, tenant);
167
- const pairs = fieldPairsByScope.get(key) ?? [];
168
- const seen = seenPairsByScope.get(key) ?? /* @__PURE__ */ new Set();
169
- const fieldPairs = buildFieldPairs(String(payload.recordId), payload.doc);
170
- for (const pair of fieldPairs) {
171
- const dedupeKey = `${pair[0]}|${pair[1]}`;
172
- if (seen.has(dedupeKey)) continue;
173
- seen.add(dedupeKey);
174
- pairs.push(pair);
175
- }
176
- fieldPairsByScope.set(key, pairs);
177
- seenPairsByScope.set(key, seen);
178
- }
179
161
  await db.transaction().execute(async (trx) => {
180
- for (const [key, bucket] of scopeBuckets.entries()) {
181
- const pairs = fieldPairsByScope.get(key) ?? [];
182
- let deleteQuery = trx.deleteFrom("search_tokens").where("entity_type", "=", payloads[0].entityType).where(sql`organization_id is not distinct from ${bucket.organizationId}`).where(sql`tenant_id is not distinct from ${bucket.tenantId}`);
183
- if (pairs.length) {
184
- deleteQuery = deleteQuery.where((eb) => eb.or(
185
- pairs.map(([rid, field]) => eb.and([
186
- eb("entity_id", "=", rid),
187
- eb("field", "=", field)
188
- ]))
189
- ));
190
- } else {
191
- deleteQuery = deleteQuery.where("entity_id", "in", Array.from(bucket.ids));
192
- }
162
+ for (const [, bucket] of scopeBuckets.entries()) {
163
+ const deleteQuery = trx.deleteFrom("search_tokens").where("entity_type", "=", payloads[0].entityType).where(sql`organization_id is not distinct from ${bucket.organizationId}`).where(sql`tenant_id is not distinct from ${bucket.tenantId}`).where("entity_id", "in", Array.from(bucket.ids));
193
164
  await deleteQuery.execute();
194
165
  }
195
166
  const payloadWithTimestamps = rows.map((row) => ({ ...row, created_at: sql`now()` }));
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/search-tokens.ts"],
4
- "sourcesContent": ["import { type Kysely, sql } from 'kysely'\nimport { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst INSERT_BATCH_SIZE = 500\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n if (size <= 0) return [items]\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\nexport type SearchTokenRow = {\n entity_type: string\n entity_id: string\n organization_id: string | null\n tenant_id: string | null\n field: string\n token_hash: string\n token?: string | null\n}\n\ntype BuildTokenOptions = {\n entityType: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n doc?: Record<string, unknown> | null\n config?: SearchConfig\n}\n\nconst DEFAULT_SCOPE = { organizationId: null, tenantId: null }\ntype EntityFieldPair = [string, string]\n\nexport const isSearchDebugEnabled = (): boolean => {\n return parseBooleanToken(process.env.OM_SEARCH_DEBUG ?? '') === true\n}\n\nconst debug = (event: string, payload: Record<string, unknown>) => {\n if (!isSearchDebugEnabled()) return\n try {\n // eslint-disable-next-line no-console\n console.debug(`[search-tokens] ${event}`, payload)\n } catch {\n // ignore\n }\n}\n\nfunction collectTextValues(value: unknown): string[] {\n if (typeof value === 'string') return [value]\n if (Array.isArray(value)) {\n const out: string[] = []\n for (const entry of value) {\n if (typeof entry === 'string') out.push(entry)\n }\n return out\n }\n return []\n}\n\nfunction shouldIndexField(field: string, value: unknown, config: SearchConfig): boolean {\n if (typeof value !== 'string' && !Array.isArray(value)) return false\n const lower = field.toLowerCase()\n if (lower === 'id' || lower.endsWith('_id') || lower.endsWith('.id')) return false\n if (lower.endsWith('_at')) return false\n if (['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)) return false\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return false\n const values = collectTextValues(value)\n if (!values.length) return false\n return values.some((text) => tokenizeText(text, config).tokens.length > 0)\n}\n\nexport function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[] {\n const config = params.config ?? resolveSearchConfig()\n if (!config.enabled) return []\n if (!params.doc) return []\n const tokens: SearchTokenRow[] = []\n const capturePairs = isSearchDebugEnabled() && params.entityType === 'customers:customer_deal'\n const debugPairs: Array<{ field: string; hash: string }> = []\n const scope = {\n organizationId: params.organizationId ?? DEFAULT_SCOPE.organizationId,\n tenantId: params.tenantId ?? DEFAULT_SCOPE.tenantId,\n }\n\n for (const [field, rawValue] of Object.entries(params.doc)) {\n if (!shouldIndexField(field, rawValue, config)) continue\n const values = collectTextValues(rawValue)\n const seen = new Set<string>()\n for (const text of values) {\n const { tokens: textTokens, hashes } = tokenizeText(text, config)\n for (let i = 0; i < textTokens.length; i += 1) {\n const token = textTokens[i]\n const hash = hashes[i]\n const dedupeKey = `${field}|${hash}`\n if (seen.has(dedupeKey)) continue\n seen.add(dedupeKey)\n debug('token.generated', { entityType: params.entityType, recordId: params.recordId, field, hash })\n tokens.push({\n entity_type: params.entityType,\n entity_id: String(params.recordId),\n organization_id: scope.organizationId,\n tenant_id: scope.tenantId,\n field,\n token_hash: hash,\n token: config.storeRawTokens ? token : null,\n })\n if (capturePairs) {\n debugPairs.push({ field, hash })\n }\n }\n }\n }\n if (capturePairs) {\n debug('deal.tokens', {\n entityType: params.entityType,\n recordId: params.recordId,\n tokenCount: debugPairs.length,\n tokens: debugPairs,\n })\n }\n debug('doc.completed', { entityType: params.entityType, recordId: params.recordId, tokenCount: tokens.length })\n\n return tokens\n}\n\nfunction buildFieldPairs(recordId: string, doc?: Record<string, unknown> | null): EntityFieldPair[] {\n if (!doc) return []\n const pairs: EntityFieldPair[] = []\n const dedupe = new Set<string>()\n for (const field of Object.keys(doc)) {\n const key = `${recordId}|${field}`\n if (dedupe.has(key)) continue\n dedupe.add(key)\n pairs.push([recordId, field])\n }\n return pairs\n}\n\nexport async function replaceSearchTokensForRecord(\n db: Kysely<any>,\n params: BuildTokenOptions\n): Promise<void> {\n const rows = buildSearchTokenRows(params)\n const config = params.config ?? resolveSearchConfig()\n if (!config.enabled) return\n const organizationId = params.organizationId ?? null\n const tenantId = params.tenantId ?? null\n const fieldPairs = buildFieldPairs(String(params.recordId), params.doc)\n\n await db.transaction().execute(async (trx) => {\n let deleteQuery = trx\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', params.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n if (fieldPairs.length) {\n deleteQuery = deleteQuery.where((eb: any) => eb.or(\n fieldPairs.map(([rid, field]) => eb.and([\n eb('entity_id' as any, '=', rid),\n eb('field' as any, '=', field),\n ])),\n ))\n } else {\n deleteQuery = deleteQuery.where('entity_id' as any, '=', String(params.recordId))\n }\n await deleteQuery.execute()\n if (!rows.length) return\n const payloads = rows.map((row) => ({ ...row, created_at: sql`now()` }))\n for (const batch of chunk(payloads, INSERT_BATCH_SIZE)) {\n await trx.insertInto('search_tokens' as any).values(batch as any).execute()\n }\n })\n}\n\nexport async function deleteSearchTokensForRecord(\n db: Kysely<any>,\n params: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null }\n): Promise<void> {\n const organizationId = params.organizationId ?? null\n const tenantId = params.tenantId ?? null\n await db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', params.entityType)\n .where('entity_id' as any, '=', String(params.recordId))\n .where(sql<boolean>`organization_id is not distinct from ${organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n .execute()\n}\n\nexport async function replaceSearchTokensForBatch(\n db: Kysely<any>,\n payloads: Array<BuildTokenOptions & { doc: Record<string, unknown> }>\n): Promise<void> {\n if (!payloads.length) return\n const config = resolveSearchConfig()\n if (!config.enabled) return\n\n const rows = payloads.flatMap((payload) => buildSearchTokenRows({ ...payload, config }))\n if (!rows.length) {\n const entityType = payloads[0]?.entityType\n if (!entityType) return\n const ids = payloads.map((p) => String(p.recordId))\n await db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', entityType)\n .where('entity_id' as any, 'in', ids)\n .execute()\n return\n }\n\n const scopeKey = (org: string | null, tenant: string | null) => `${org ?? '__null__'}|${tenant ?? '__null__'}`\n const scopeBuckets = new Map<string, { organizationId: string | null; tenantId: string | null; ids: Set<string> }>()\n const fieldPairsByScope = new Map<string, EntityFieldPair[]>()\n const seenPairsByScope = new Map<string, Set<string>>()\n\n for (const payload of payloads) {\n const org = payload.organizationId ?? null\n const tenant = payload.tenantId ?? null\n const key = scopeKey(org, tenant)\n const bucket = scopeBuckets.get(key) ?? { organizationId: org, tenantId: tenant, ids: new Set<string>() }\n bucket.ids.add(String(payload.recordId))\n scopeBuckets.set(key, bucket)\n }\n\n for (const payload of payloads) {\n const org = payload.organizationId ?? null\n const tenant = payload.tenantId ?? null\n const key = scopeKey(org, tenant)\n const pairs = fieldPairsByScope.get(key) ?? []\n const seen = seenPairsByScope.get(key) ?? new Set<string>()\n const fieldPairs = buildFieldPairs(String(payload.recordId), payload.doc)\n for (const pair of fieldPairs) {\n const dedupeKey = `${pair[0]}|${pair[1]}`\n if (seen.has(dedupeKey)) continue\n seen.add(dedupeKey)\n pairs.push(pair)\n }\n fieldPairsByScope.set(key, pairs)\n seenPairsByScope.set(key, seen)\n }\n\n await db.transaction().execute(async (trx) => {\n for (const [key, bucket] of scopeBuckets.entries()) {\n const pairs = fieldPairsByScope.get(key) ?? []\n let deleteQuery = trx\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', payloads[0].entityType)\n .where(sql<boolean>`organization_id is not distinct from ${bucket.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${bucket.tenantId}`)\n if (pairs.length) {\n deleteQuery = deleteQuery.where((eb: any) => eb.or(\n pairs.map(([rid, field]) => eb.and([\n eb('entity_id' as any, '=', rid),\n eb('field' as any, '=', field),\n ])),\n ))\n } else {\n deleteQuery = deleteQuery.where('entity_id' as any, 'in', Array.from(bucket.ids))\n }\n await deleteQuery.execute()\n }\n const payloadWithTimestamps = rows.map((row) => ({ ...row, created_at: sql`now()` }))\n for (const batch of chunk(payloadWithTimestamps, INSERT_BATCH_SIZE)) {\n await trx.insertInto('search_tokens' as any).values(batch as any).execute()\n }\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type Kysely, sql } from 'kysely'\nimport { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst INSERT_BATCH_SIZE = 500\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n if (size <= 0) return [items]\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\nexport type SearchTokenRow = {\n entity_type: string\n entity_id: string\n organization_id: string | null\n tenant_id: string | null\n field: string\n token_hash: string\n token?: string | null\n}\n\ntype BuildTokenOptions = {\n entityType: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n doc?: Record<string, unknown> | null\n config?: SearchConfig\n}\n\nconst DEFAULT_SCOPE = { organizationId: null, tenantId: null }\ntype EntityFieldPair = [string, string]\n\nexport const isSearchDebugEnabled = (): boolean => {\n return parseBooleanToken(process.env.OM_SEARCH_DEBUG ?? '') === true\n}\n\nconst debug = (event: string, payload: Record<string, unknown>) => {\n if (!isSearchDebugEnabled()) return\n try {\n // eslint-disable-next-line no-console\n console.debug(`[search-tokens] ${event}`, payload)\n } catch {\n // ignore\n }\n}\n\nfunction collectTextValues(value: unknown): string[] {\n if (typeof value === 'string') return [value]\n if (Array.isArray(value)) {\n const out: string[] = []\n for (const entry of value) {\n if (typeof entry === 'string') out.push(entry)\n }\n return out\n }\n return []\n}\n\nfunction shouldIndexField(field: string, value: unknown, config: SearchConfig): boolean {\n if (typeof value !== 'string' && !Array.isArray(value)) return false\n const lower = field.toLowerCase()\n if (lower === 'id' || lower.endsWith('_id') || lower.endsWith('.id')) return false\n if (lower.endsWith('_at')) return false\n if (['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)) return false\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return false\n const values = collectTextValues(value)\n if (!values.length) return false\n return values.some((text) => tokenizeText(text, config).tokens.length > 0)\n}\n\nexport function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[] {\n const config = params.config ?? resolveSearchConfig()\n if (!config.enabled) return []\n if (!params.doc) return []\n const tokens: SearchTokenRow[] = []\n const capturePairs = isSearchDebugEnabled() && params.entityType === 'customers:customer_deal'\n const debugPairs: Array<{ field: string; hash: string }> = []\n const scope = {\n organizationId: params.organizationId ?? DEFAULT_SCOPE.organizationId,\n tenantId: params.tenantId ?? DEFAULT_SCOPE.tenantId,\n }\n\n for (const [field, rawValue] of Object.entries(params.doc)) {\n if (!shouldIndexField(field, rawValue, config)) continue\n const values = collectTextValues(rawValue)\n const seen = new Set<string>()\n for (const text of values) {\n const { tokens: textTokens, hashes } = tokenizeText(text, config)\n for (let i = 0; i < textTokens.length; i += 1) {\n const token = textTokens[i]\n const hash = hashes[i]\n const dedupeKey = `${field}|${hash}`\n if (seen.has(dedupeKey)) continue\n seen.add(dedupeKey)\n debug('token.generated', { entityType: params.entityType, recordId: params.recordId, field, hash })\n tokens.push({\n entity_type: params.entityType,\n entity_id: String(params.recordId),\n organization_id: scope.organizationId,\n tenant_id: scope.tenantId,\n field,\n token_hash: hash,\n token: config.storeRawTokens ? token : null,\n })\n if (capturePairs) {\n debugPairs.push({ field, hash })\n }\n }\n }\n }\n if (capturePairs) {\n debug('deal.tokens', {\n entityType: params.entityType,\n recordId: params.recordId,\n tokenCount: debugPairs.length,\n tokens: debugPairs,\n })\n }\n debug('doc.completed', { entityType: params.entityType, recordId: params.recordId, tokenCount: tokens.length })\n\n return tokens\n}\n\nfunction buildFieldPairs(recordId: string, doc?: Record<string, unknown> | null): EntityFieldPair[] {\n if (!doc) return []\n const pairs: EntityFieldPair[] = []\n const dedupe = new Set<string>()\n for (const field of Object.keys(doc)) {\n const key = `${recordId}|${field}`\n if (dedupe.has(key)) continue\n dedupe.add(key)\n pairs.push([recordId, field])\n }\n return pairs\n}\n\nexport async function replaceSearchTokensForRecord(\n db: Kysely<any>,\n params: BuildTokenOptions\n): Promise<void> {\n const rows = buildSearchTokenRows(params)\n const config = params.config ?? resolveSearchConfig()\n if (!config.enabled) return\n const organizationId = params.organizationId ?? null\n const tenantId = params.tenantId ?? null\n const fieldPairs = buildFieldPairs(String(params.recordId), params.doc)\n\n await db.transaction().execute(async (trx) => {\n let deleteQuery = trx\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', params.entityType)\n .where(sql<boolean>`organization_id is not distinct from ${organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n if (fieldPairs.length) {\n deleteQuery = deleteQuery.where((eb: any) => eb.or(\n fieldPairs.map(([rid, field]) => eb.and([\n eb('entity_id' as any, '=', rid),\n eb('field' as any, '=', field),\n ])),\n ))\n } else {\n deleteQuery = deleteQuery.where('entity_id' as any, '=', String(params.recordId))\n }\n await deleteQuery.execute()\n if (!rows.length) return\n const payloads = rows.map((row) => ({ ...row, created_at: sql`now()` }))\n for (const batch of chunk(payloads, INSERT_BATCH_SIZE)) {\n await trx.insertInto('search_tokens' as any).values(batch as any).execute()\n }\n })\n}\n\nexport async function deleteSearchTokensForRecord(\n db: Kysely<any>,\n params: { entityType: string; recordId: string; organizationId?: string | null; tenantId?: string | null }\n): Promise<void> {\n const organizationId = params.organizationId ?? null\n const tenantId = params.tenantId ?? null\n await db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', params.entityType)\n .where('entity_id' as any, '=', String(params.recordId))\n .where(sql<boolean>`organization_id is not distinct from ${organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n .execute()\n}\n\nexport async function replaceSearchTokensForBatch(\n db: Kysely<any>,\n payloads: Array<BuildTokenOptions & { doc: Record<string, unknown> }>\n): Promise<void> {\n if (!payloads.length) return\n const config = resolveSearchConfig()\n if (!config.enabled) return\n\n const rows = payloads.flatMap((payload) => buildSearchTokenRows({ ...payload, config }))\n if (!rows.length) {\n const entityType = payloads[0]?.entityType\n if (!entityType) return\n const ids = payloads.map((p) => String(p.recordId))\n await db\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', entityType)\n .where('entity_id' as any, 'in', ids)\n .execute()\n return\n }\n\n const scopeKey = (org: string | null, tenant: string | null) => `${org ?? '__null__'}|${tenant ?? '__null__'}`\n const scopeBuckets = new Map<string, { organizationId: string | null; tenantId: string | null; ids: Set<string> }>()\n\n for (const payload of payloads) {\n const org = payload.organizationId ?? null\n const tenant = payload.tenantId ?? null\n const key = scopeKey(org, tenant)\n const bucket = scopeBuckets.get(key) ?? { organizationId: org, tenantId: tenant, ids: new Set<string>() }\n bucket.ids.add(String(payload.recordId))\n scopeBuckets.set(key, bucket)\n }\n\n await db.transaction().execute(async (trx) => {\n for (const [, bucket] of scopeBuckets.entries()) {\n // Delete by entity_id: a batch replaces all of a record's tokens, and a per-field OR over the\n // whole batch overflows the query compiler's call stack on large batches.\n const deleteQuery = trx\n .deleteFrom('search_tokens' as any)\n .where('entity_type' as any, '=', payloads[0].entityType)\n .where(sql<boolean>`organization_id is not distinct from ${bucket.organizationId}`)\n .where(sql<boolean>`tenant_id is not distinct from ${bucket.tenantId}`)\n .where('entity_id' as any, 'in', Array.from(bucket.ids))\n await deleteQuery.execute()\n }\n const payloadWithTimestamps = rows.map((row) => ({ ...row, created_at: sql`now()` }))\n for (const batch of chunk(payloadWithTimestamps, INSERT_BATCH_SIZE)) {\n await trx.insertInto('search_tokens' as any).values(batch as any).execute()\n }\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -800,14 +800,14 @@ const syncExcelCustomersAdapter = {
800
800
  const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0;
801
801
  const commandContext = buildCommandContext(container, input.scope);
802
802
  const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope);
803
+ const emailDedupeIndex = await buildEmailDedupeIndex({
804
+ em,
805
+ rows: document.rows.slice(startOffset),
806
+ mapping: input.mapping,
807
+ scope: input.scope
808
+ });
803
809
  for (let offset = startOffset, batchIndex = 0; offset < document.rows.length; offset += input.batchSize, batchIndex += 1) {
804
810
  const batchRows = document.rows.slice(offset, offset + input.batchSize);
805
- const emailDedupeIndex = await buildEmailDedupeIndex({
806
- em,
807
- rows: batchRows,
808
- mapping: input.mapping,
809
- scope: input.scope
810
- });
811
811
  const items = [];
812
812
  for (let index = 0; index < batchRows.length; index += 1) {
813
813
  items.push(await processRow({