@open-mercato/core 0.6.7-develop.6696.1.4236709f7e → 0.6.7-develop.6725.1.07a748cfc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/admin/nav.js +14 -4
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +8 -19
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +4 -1
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +31 -14
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +12 -18
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/cli.js +11 -4
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/detail/ActiveDealCard.js +2 -1
- package/dist/modules/customers/components/detail/ActiveDealCard.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +6 -5
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/dashboard/helpers.js +4 -3
- package/dist/modules/customers/components/detail/dashboard/helpers.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +14 -10
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/dealStatus.js +38 -0
- package/dist/modules/customers/lib/dealStatus.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/lib/aggregations.js +76 -40
- package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
- package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
- package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +228 -5
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
- package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
- package/dist/modules/messages/lib/searchLookup.js +8 -16
- package/dist/modules/messages/lib/searchLookup.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +5 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/di.js +17 -1
- package/dist/modules/payment_gateways/di.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +8 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js +58 -0
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js.map +7 -0
- package/dist/modules/query_index/api/openapi.js +13 -1
- package/dist/modules/query_index/api/openapi.js.map +2 -2
- package/dist/modules/query_index/api/status.js +165 -105
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +11 -4
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/document.js +12 -5
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +36 -4
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +1 -1
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +7 -4
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +14 -3
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +22 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
- package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
- package/dist/modules/sales/data/validators.js +25 -4
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/di.js +7 -0
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/services/paymentOrderTotalResolver.js +52 -0
- package/dist/modules/sales/services/paymentOrderTotalResolver.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/api/admin/nav.ts +24 -4
- package/src/modules/auth/api/users/route.ts +8 -28
- package/src/modules/auth/frontend/login.tsx +4 -1
- package/src/modules/auth/lib/backendChrome.tsx +51 -13
- package/src/modules/customers/api/companies/[id]/route.ts +3 -4
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -2
- package/src/modules/customers/api/utils.ts +12 -31
- package/src/modules/customers/cli.ts +11 -4
- package/src/modules/customers/components/detail/ActiveDealCard.tsx +2 -1
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +6 -5
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
- package/src/modules/customers/components/detail/dashboard/helpers.ts +4 -3
- package/src/modules/customers/components/formConfig.tsx +12 -8
- package/src/modules/customers/lib/dealStatus.ts +48 -0
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
- package/src/modules/dashboards/lib/aggregations.ts +122 -43
- package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
- package/src/modules/dashboards/services/widgetDataService.ts +342 -4
- package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
- package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
- package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
- package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
- package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
- package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
- package/src/modules/messages/lib/searchLookup.ts +11 -34
- package/src/modules/payment_gateways/api/sessions/route.ts +5 -0
- package/src/modules/payment_gateways/di.ts +28 -2
- package/src/modules/payment_gateways/i18n/de.json +3 -0
- package/src/modules/payment_gateways/i18n/en.json +3 -0
- package/src/modules/payment_gateways/i18n/es.json +3 -0
- package/src/modules/payment_gateways/i18n/pl.json +3 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +14 -0
- package/src/modules/payment_gateways/lib/order-amount-reconciliation.ts +93 -0
- package/src/modules/query_index/api/openapi.ts +12 -0
- package/src/modules/query_index/api/status.ts +238 -136
- package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
- package/src/modules/query_index/i18n/de.json +5 -3
- package/src/modules/query_index/i18n/en.json +5 -3
- package/src/modules/query_index/i18n/es.json +5 -3
- package/src/modules/query_index/i18n/pl.json +5 -3
- package/src/modules/query_index/lib/batch.ts +11 -4
- package/src/modules/query_index/lib/document.ts +35 -4
- package/src/modules/query_index/lib/engine.ts +53 -5
- package/src/modules/query_index/lib/indexer.ts +1 -1
- package/src/modules/query_index/lib/search-tokens.ts +13 -4
- package/src/modules/sales/api/documents/factory.ts +28 -3
- package/src/modules/sales/commands/documents.ts +26 -2
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
- package/src/modules/sales/data/validators.ts +50 -5
- package/src/modules/sales/di.ts +9 -0
- package/src/modules/sales/services/paymentOrderTotalResolver.ts +67 -0
|
@@ -15,6 +15,24 @@ import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
|
15
15
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
16
16
|
const logger = createLogger("query_index").child({ component: "indexes-table" });
|
|
17
17
|
const MUTATION_CONTEXT_ID = "query_index.status.list:actions";
|
|
18
|
+
const BACKEND_FILTER_ID = "backend";
|
|
19
|
+
const PAGE_SIZE = 50;
|
|
20
|
+
function sortValue(row, columnId) {
|
|
21
|
+
if (columnId === "entityId") return row.entityId;
|
|
22
|
+
if (columnId === "baseCount") return row.baseCount ?? 0;
|
|
23
|
+
if (columnId === "indexCount") return row.indexCount ?? 0;
|
|
24
|
+
if (columnId === "vectorCount") return isVectorReportable(row) ? row.vectorCount ?? 0 : -1;
|
|
25
|
+
if (columnId === "fulltextCount") return row.fulltextEnabled ? row.fulltextCount ?? 0 : -1;
|
|
26
|
+
return row.entityId;
|
|
27
|
+
}
|
|
28
|
+
function isVectorReportable(row) {
|
|
29
|
+
return row.vectorEnabled && (row.vectorIndexingActive ?? true);
|
|
30
|
+
}
|
|
31
|
+
function matchesBackendFilter(row, backend) {
|
|
32
|
+
if (backend === "vector") return row.vectorEnabled;
|
|
33
|
+
if (backend === "fulltext") return row.fulltextEnabled;
|
|
34
|
+
return Boolean(row.hasCustomFields);
|
|
35
|
+
}
|
|
18
36
|
function formatCount(value) {
|
|
19
37
|
if (value == null) return "\u2014";
|
|
20
38
|
return value.toLocaleString();
|
|
@@ -30,15 +48,17 @@ function formatProgressLabel(processed, total, t) {
|
|
|
30
48
|
if (totalText) return t("query_index.table.status.progress", { processed: processedText, total: totalText });
|
|
31
49
|
return t("query_index.table.status.progressSingle", { processed: processedText });
|
|
32
50
|
}
|
|
33
|
-
function translateJobStatus(t, status, ok) {
|
|
34
|
-
|
|
51
|
+
function translateJobStatus(t, status, ok, measured) {
|
|
52
|
+
const idleLabel = () => {
|
|
53
|
+
if (!measured) return t("query_index.table.status.not_measured");
|
|
35
54
|
return ok ? t("query_index.table.status.in_sync") : t("query_index.table.status.out_of_sync");
|
|
36
|
-
}
|
|
55
|
+
};
|
|
56
|
+
if (!status || status === "idle") return idleLabel();
|
|
37
57
|
if (status === "reindexing") return t("query_index.table.status.reindexing");
|
|
38
58
|
if (status === "purging") return t("query_index.table.status.purging");
|
|
39
59
|
if (status === "stalled") return t("query_index.table.status.stalled");
|
|
40
60
|
if (status === "failed") return t("query_index.table.status.failed");
|
|
41
|
-
return
|
|
61
|
+
return idleLabel();
|
|
42
62
|
}
|
|
43
63
|
function translateScopeStatus(t, status) {
|
|
44
64
|
if (status === "reindexing") return t("query_index.table.status.scope.reindexing");
|
|
@@ -49,8 +69,15 @@ function translateScopeStatus(t, status) {
|
|
|
49
69
|
}
|
|
50
70
|
function createColumns(t) {
|
|
51
71
|
return [
|
|
52
|
-
{
|
|
53
|
-
|
|
72
|
+
{
|
|
73
|
+
id: "entityId",
|
|
74
|
+
header: () => t("query_index.table.columns.entity"),
|
|
75
|
+
accessorKey: "entityId",
|
|
76
|
+
// The entity id is the only identifier on this page and ids run to ~45 characters
|
|
77
|
+
// (`catalog:catalog_product_category_assignment`). DataTable's default 150px clamp
|
|
78
|
+
// ellipsised them into indistinguishable `customers:customer…` rows.
|
|
79
|
+
meta: { priority: 1, maxWidth: "340px" }
|
|
80
|
+
},
|
|
54
81
|
{
|
|
55
82
|
id: "baseCount",
|
|
56
83
|
header: () => t("query_index.table.columns.records"),
|
|
@@ -68,10 +95,10 @@ function createColumns(t) {
|
|
|
68
95
|
{
|
|
69
96
|
id: "vectorCount",
|
|
70
97
|
header: () => t("query_index.table.columns.vector"),
|
|
71
|
-
accessorFn: (row) => row
|
|
98
|
+
accessorFn: (row) => isVectorReportable(row) ? row.vectorCount ?? 0 : -1,
|
|
72
99
|
cell: ({ row }) => {
|
|
73
100
|
const record = row.original;
|
|
74
|
-
if (!record
|
|
101
|
+
if (!isVectorReportable(record)) return /* @__PURE__ */ jsx("span", { children: "\u2014" });
|
|
75
102
|
const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount;
|
|
76
103
|
const display = formatCount(record.vectorCount);
|
|
77
104
|
const className = ok ? "text-status-success-text" : "text-status-warning-text";
|
|
@@ -100,40 +127,41 @@ function createColumns(t) {
|
|
|
100
127
|
const record = row.original;
|
|
101
128
|
const job = record.job;
|
|
102
129
|
const partitions = job?.partitions ?? [];
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
130
|
+
const measured = record.baseCount != null || record.indexCount != null;
|
|
131
|
+
const scopeStatus = job?.scope?.status ?? null;
|
|
132
|
+
const scopeInFlight = scopeStatus != null && scopeStatus !== "completed";
|
|
133
|
+
const effectiveStatus = job ? job.status !== "idle" ? job.status : scopeInFlight ? scopeStatus : "idle" : void 0;
|
|
134
|
+
const indexOk = record.queryIndexOk ?? record.ok;
|
|
135
|
+
const ok = indexOk && (!job || effectiveStatus === "idle");
|
|
136
|
+
const jobFailed = effectiveStatus === "failed";
|
|
137
|
+
const statusText = translateJobStatus(t, effectiveStatus, ok, measured);
|
|
138
|
+
const jobInFlight = Boolean(effectiveStatus && effectiveStatus !== "idle") || partitions.some((part) => part.status !== "completed");
|
|
139
|
+
const jobProgress = jobInFlight && job ? formatProgressLabel(job.processedCount ?? null, job.totalCount ?? null, t) : null;
|
|
107
140
|
const label = jobProgress ? t("query_index.table.status.withProgress", { status: statusText, progress: jobProgress }) : statusText;
|
|
108
141
|
let variant = "neutral";
|
|
109
142
|
if (job) {
|
|
110
|
-
if (
|
|
111
|
-
else if (
|
|
143
|
+
if (effectiveStatus === "stalled" || jobFailed) variant = "error";
|
|
144
|
+
else if (effectiveStatus === "reindexing" || effectiveStatus === "purging") variant = "warning";
|
|
112
145
|
else variant = ok ? "success" : "neutral";
|
|
113
146
|
} else {
|
|
114
147
|
variant = ok ? "success" : "neutral";
|
|
115
148
|
}
|
|
116
149
|
const lines = [];
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
if (jobInFlight) {
|
|
151
|
+
if (job?.scope && partitions.length <= 1) {
|
|
152
|
+
const scopeStatusText = translateScopeStatus(t, scopeStatus);
|
|
153
|
+
const scopeProgress = formatProgressLabel(job.scope.processedCount ?? null, job.scope.totalCount ?? null, t);
|
|
154
|
+
const scopeLabel = t("query_index.table.status.scopeLabel");
|
|
155
|
+
lines.push(`${scopeLabel}: ${scopeStatusText}${scopeProgress ? ` (${scopeProgress})` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
if (partitions.length > 1) {
|
|
158
|
+
for (const part of partitions) {
|
|
159
|
+
const partitionLabel = part.partitionIndex != null ? t("query_index.table.status.partitionLabel", { index: Number(part.partitionIndex) + 1 }) : t("query_index.table.status.scopeLabel");
|
|
160
|
+
const partitionStatus = translateScopeStatus(t, part.status);
|
|
161
|
+
const partitionProgress = formatProgressLabel(part.processedCount ?? null, part.totalCount ?? null, t);
|
|
162
|
+
lines.push(`${partitionLabel}: ${partitionStatus}${partitionProgress ? ` (${partitionProgress})` : ""}`);
|
|
163
|
+
}
|
|
129
164
|
}
|
|
130
|
-
}
|
|
131
|
-
if (record.vectorEnabled) {
|
|
132
|
-
const vectorLabel = t("query_index.table.status.vectorLabel");
|
|
133
|
-
const vectorCount = formatCount(record.vectorCount);
|
|
134
|
-
const vectorTotal = record.baseCount != null ? formatCount(record.baseCount) : null;
|
|
135
|
-
const vectorValue = vectorTotal ? t("query_index.table.status.vectorValue", { count: vectorCount, total: vectorTotal }) : vectorCount;
|
|
136
|
-
lines.push(`${vectorLabel}: ${vectorValue}`);
|
|
137
165
|
}
|
|
138
166
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
139
167
|
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(StatusBadge, { variant, dot: true, children: label }) }),
|
|
@@ -146,14 +174,27 @@ function createColumns(t) {
|
|
|
146
174
|
}
|
|
147
175
|
function QueryIndexesTable() {
|
|
148
176
|
const [sorting, setSorting] = React.useState([{ id: "entityId", desc: false }]);
|
|
149
|
-
const [page, setPage] = React.useState(1);
|
|
150
177
|
const [search, setSearch] = React.useState("");
|
|
178
|
+
const [page, setPage] = React.useState(1);
|
|
179
|
+
const [filterValues, setFilterValues] = React.useState({});
|
|
151
180
|
const qc = useQueryClient();
|
|
152
181
|
const scopeVersion = useOrganizationScopeVersion();
|
|
153
182
|
const [refreshSeq, setRefreshSeq] = React.useState(0);
|
|
154
183
|
const t = useT();
|
|
155
184
|
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
156
185
|
const columns = React.useMemo(() => createColumns(t), [t]);
|
|
186
|
+
const filters = React.useMemo(() => [
|
|
187
|
+
{
|
|
188
|
+
id: BACKEND_FILTER_ID,
|
|
189
|
+
label: t("query_index.table.filters.backend"),
|
|
190
|
+
type: "select",
|
|
191
|
+
options: [
|
|
192
|
+
{ label: t("query_index.table.filters.backendOptions.vector"), value: "vector" },
|
|
193
|
+
{ label: t("query_index.table.filters.backendOptions.fulltext"), value: "fulltext" },
|
|
194
|
+
{ label: t("query_index.table.filters.backendOptions.customFields"), value: "custom_fields" }
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
], [t]);
|
|
157
198
|
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
158
199
|
contextId: MUTATION_CONTEXT_ID,
|
|
159
200
|
blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
@@ -172,11 +213,43 @@ function QueryIndexesTable() {
|
|
|
172
213
|
refetchInterval: 4e3
|
|
173
214
|
});
|
|
174
215
|
const rowsAll = data?.items || [];
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const q = search.toLowerCase();
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
const matchedRows = React.useMemo(() => {
|
|
217
|
+
const backend = filterValues[BACKEND_FILTER_ID];
|
|
218
|
+
const q = search.trim().toLowerCase();
|
|
219
|
+
if (!q && !backend) return rowsAll;
|
|
220
|
+
return rowsAll.filter((row) => {
|
|
221
|
+
if (q && !row.entityId.toLowerCase().includes(q)) return false;
|
|
222
|
+
if (backend && !matchesBackendFilter(row, backend)) return false;
|
|
223
|
+
return true;
|
|
224
|
+
});
|
|
225
|
+
}, [rowsAll, search, filterValues]);
|
|
226
|
+
const sortedRows = React.useMemo(() => {
|
|
227
|
+
const sort = sorting[0];
|
|
228
|
+
if (!sort) return matchedRows;
|
|
229
|
+
const direction = sort.desc ? -1 : 1;
|
|
230
|
+
return [...matchedRows].sort((a, b) => {
|
|
231
|
+
const left = sortValue(a, sort.id);
|
|
232
|
+
const right = sortValue(b, sort.id);
|
|
233
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
234
|
+
return String(left).localeCompare(String(right)) * direction;
|
|
235
|
+
}
|
|
236
|
+
return (Number(left) - Number(right)) * direction;
|
|
237
|
+
});
|
|
238
|
+
}, [matchedRows, sorting]);
|
|
239
|
+
const total = sortedRows.length;
|
|
240
|
+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
241
|
+
const currentPage = Math.min(page, totalPages);
|
|
242
|
+
const rows = React.useMemo(
|
|
243
|
+
() => sortedRows.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE),
|
|
244
|
+
[sortedRows, currentPage]
|
|
245
|
+
);
|
|
246
|
+
const pagination = React.useMemo(() => ({
|
|
247
|
+
page: currentPage,
|
|
248
|
+
pageSize: PAGE_SIZE,
|
|
249
|
+
total,
|
|
250
|
+
totalPages,
|
|
251
|
+
onPageChange: setPage
|
|
252
|
+
}), [currentPage, total, totalPages]);
|
|
180
253
|
const trigger = React.useCallback(
|
|
181
254
|
async (action, entityId, opts) => {
|
|
182
255
|
const body = { entityType: entityId };
|
|
@@ -307,9 +380,23 @@ function QueryIndexesTable() {
|
|
|
307
380
|
setSearch(value);
|
|
308
381
|
setPage(1);
|
|
309
382
|
},
|
|
383
|
+
filters,
|
|
384
|
+
filterValues,
|
|
385
|
+
onFiltersApply: (values) => {
|
|
386
|
+
setFilterValues(values);
|
|
387
|
+
setPage(1);
|
|
388
|
+
},
|
|
389
|
+
onFiltersClear: () => {
|
|
390
|
+
setFilterValues({});
|
|
391
|
+
setPage(1);
|
|
392
|
+
},
|
|
310
393
|
sortable: true,
|
|
394
|
+
manualSorting: true,
|
|
311
395
|
sorting,
|
|
312
|
-
onSortingChange:
|
|
396
|
+
onSortingChange: (next) => {
|
|
397
|
+
setSorting(next);
|
|
398
|
+
setPage(1);
|
|
399
|
+
},
|
|
313
400
|
perspective: { tableId: "query_index.status.list" },
|
|
314
401
|
rowActions: (row) => {
|
|
315
402
|
const items = [
|
|
@@ -358,7 +445,7 @@ function QueryIndexesTable() {
|
|
|
358
445
|
}
|
|
359
446
|
return /* @__PURE__ */ jsx(RowActions, { items });
|
|
360
447
|
},
|
|
361
|
-
pagination
|
|
448
|
+
pagination,
|
|
362
449
|
isLoading
|
|
363
450
|
}
|
|
364
451
|
),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/components/QueryIndexesTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('query_index').child({ component: 'indexes-table' })\n\nconst MUTATION_CONTEXT_ID = 'query_index.status.list:actions'\n\ntype Translator = (key: string, params?: Record<string, string | number>) => string\n\ntype PartitionStatus = {\n partitionIndex: number | null\n partitionCount: number | null\n status: 'reindexing' | 'purging' | 'stalled' | 'completed' | 'failed'\n processedCount?: number | null\n totalCount?: number | null\n heartbeatAt?: string | null\n startedAt?: string | null\n finishedAt?: string | null\n}\n\ntype JobStatus = {\n status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed'\n startedAt?: string | null\n finishedAt?: string | null\n heartbeatAt?: string | null\n processedCount?: number | null\n totalCount?: number | null\n partitions?: PartitionStatus[]\n scope?: {\n status?: 'reindexing' | 'purging' | 'stalled' | 'completed' | 'failed' | null\n processedCount?: number | null\n totalCount?: number | null\n } | null\n}\n\ntype Row = {\n entityId: string\n label: string\n baseCount: number | null\n indexCount: number | null\n vectorCount: number | null\n vectorEnabled: boolean\n fulltextCount: number | null\n fulltextEnabled: boolean\n ok: boolean\n job?: JobStatus\n}\n\ntype Resp = { items: Row[] }\n\nfunction formatCount(value: number | null): string {\n if (value == null) return '\u2014'\n return value.toLocaleString()\n}\n\nfunction formatNumeric(value: number | null | undefined): string | null {\n if (value == null) return null\n return Number(value).toLocaleString()\n}\n\nfunction formatProgressLabel(\n processed: number | null | undefined,\n total: number | null | undefined,\n t: Translator,\n): string | null {\n const processedText = formatNumeric(processed)\n if (!processedText) return null\n const totalText = formatNumeric(total)\n if (totalText) return t('query_index.table.status.progress', { processed: processedText, total: totalText })\n return t('query_index.table.status.progressSingle', { processed: processedText })\n}\n\nfunction translateJobStatus(t: Translator, status: JobStatus['status'] | undefined, ok: boolean): string {\n if (!status || status === 'idle') {\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n }\n if (status === 'reindexing') return t('query_index.table.status.reindexing')\n if (status === 'purging') return t('query_index.table.status.purging')\n if (status === 'stalled') return t('query_index.table.status.stalled')\n if (status === 'failed') return t('query_index.table.status.failed')\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n}\n\nfunction translateScopeStatus(\n t: Translator,\n status: PartitionStatus['status'] | JobStatus['status'] | undefined | null,\n): string {\n if (status === 'reindexing') return t('query_index.table.status.scope.reindexing')\n if (status === 'purging') return t('query_index.table.status.scope.purging')\n if (status === 'stalled') return t('query_index.table.status.scope.stalled')\n if (status === 'failed') return t('query_index.table.status.scope.failed')\n return t('query_index.table.status.scope.completed')\n}\n\n\nfunction createColumns(t: Translator): ColumnDef<Row>[] {\n return [\n { id: 'entityId', header: () => t('query_index.table.columns.entity'), accessorKey: 'entityId', meta: { priority: 1 } },\n { id: 'label', header: () => t('query_index.table.columns.label'), accessorKey: 'label', meta: { priority: 2 } },\n {\n id: 'baseCount',\n header: () => t('query_index.table.columns.records'),\n accessorFn: (row) => row.baseCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.baseCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'indexCount',\n header: () => t('query_index.table.columns.indexed'),\n accessorFn: (row) => row.indexCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.indexCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'vectorCount',\n header: () => t('query_index.table.columns.vector'),\n accessorFn: (row) => (row.vectorEnabled ? row.vectorCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.vectorEnabled) return <span>\u2014</span>\n const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount\n const display = formatCount(record.vectorCount)\n const className = ok ? 'text-status-success-text' : 'text-status-warning-text'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'fulltextCount',\n header: () => t('query_index.table.columns.fulltext'),\n accessorFn: (row) => (row.fulltextEnabled ? row.fulltextCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.fulltextEnabled) return <span>\u2014</span>\n const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount\n const display = formatCount(record.fulltextCount)\n const className = ok ? 'text-status-success-text' : 'text-status-warning-text'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'status',\n header: () => t('query_index.table.columns.status'),\n cell: ({ row }) => {\n const record = row.original\n const job = record.job\n const partitions = job?.partitions ?? []\n const ok = record.ok && (!job || job.status === 'idle')\n const jobFailed = job?.status === 'failed'\n const statusText = translateJobStatus(t, job?.status, ok)\n const jobProgress = job ? formatProgressLabel(job.processedCount ?? null, job.totalCount ?? null, t) : null\n const label = jobProgress\n ? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })\n : statusText\n let variant: StatusBadgeVariant = 'neutral'\n if (job) {\n if (job.status === 'stalled' || jobFailed) variant = 'error'\n else if (job.status === 'reindexing' || job.status === 'purging') variant = 'warning'\n else variant = ok ? 'success' : 'neutral'\n } else {\n variant = ok ? 'success' : 'neutral'\n }\n\n const lines: string[] = []\n\n if (job?.scope && partitions.length <= 1) {\n const scopeStatus = translateScopeStatus(t, job.scope.status ?? null)\n const scopeProgress = formatProgressLabel(job.scope.processedCount ?? null, job.scope.totalCount ?? null, t)\n const scopeLabel = t('query_index.table.status.scopeLabel')\n lines.push(`${scopeLabel}: ${scopeStatus}${scopeProgress ? ` (${scopeProgress})` : ''}`)\n }\n\n if (partitions.length > 1) {\n for (const part of partitions) {\n const partitionLabel =\n part.partitionIndex != null\n ? t('query_index.table.status.partitionLabel', { index: Number(part.partitionIndex) + 1 })\n : t('query_index.table.status.scopeLabel')\n const partitionStatus = translateScopeStatus(t, part.status)\n const partitionProgress = formatProgressLabel(part.processedCount ?? null, part.totalCount ?? null, t)\n lines.push(`${partitionLabel}: ${partitionStatus}${partitionProgress ? ` (${partitionProgress})` : ''}`)\n }\n }\n\n if (record.vectorEnabled) {\n const vectorLabel = t('query_index.table.status.vectorLabel')\n const vectorCount = formatCount(record.vectorCount)\n const vectorTotal = record.baseCount != null ? formatCount(record.baseCount) : null\n const vectorValue = vectorTotal\n ? t('query_index.table.status.vectorValue', { count: vectorCount, total: vectorTotal })\n : vectorCount\n lines.push(`${vectorLabel}: ${vectorValue}`)\n }\n\n return (\n <div className=\"space-y-1.5\">\n <div>\n <StatusBadge variant={variant} dot>\n {label}\n </StatusBadge>\n </div>\n {lines.length > 0 && (\n <div className=\"text-xs text-muted-foreground\">\n {lines.map((line, idx) => (\n <div key={idx}>{line}</div>\n ))}\n </div>\n )}\n </div>\n )\n },\n meta: { priority: 1 },\n },\n ]\n}\n\nexport default function QueryIndexesTable() {\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'entityId', desc: false }])\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const qc = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [refreshSeq, setRefreshSeq] = React.useState(0)\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const columns = React.useMemo(() => createColumns(t), [t])\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const { data, isLoading } = useQuery<Resp>({\n queryKey: ['query-index-status', scopeVersion, refreshSeq],\n queryFn: async () => {\n const baseUrl = '/api/query_index/status'\n const url = refreshSeq > 0 ? `${baseUrl}?refresh=${refreshSeq}` : baseUrl\n return readApiResultOrThrow<Resp>(\n url,\n undefined,\n { errorMessage: t('query_index.table.errors.loadFailed') },\n )\n },\n refetchInterval: 4000,\n })\n\n const rowsAll = data?.items || []\n const rows = React.useMemo(() => {\n if (!search) return rowsAll\n const q = search.toLowerCase()\n return rowsAll.filter((r) => r.entityId.toLowerCase().includes(q) || r.label.toLowerCase().includes(q))\n }, [rowsAll, search])\n\n const trigger = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string, opts?: { force?: boolean }) => {\n const body: Record<string, unknown> = { entityType: entityId }\n if (opts?.force) body.force = true\n const actionLabel =\n action === 'purge' ? t('query_index.table.actions.purge') : t('query_index.table.actions.reindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow(`/api/query_index/${action}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n }, { errorMessage }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'query_index',\n resourceId: entityId,\n retryLastMutation,\n },\n mutationPayload: { action, entityType: entityId, force: Boolean(opts?.force) },\n })\n } catch (err) {\n // Expected operational failures (e.g. a 503 when a search backend is not\n // configured) \u2014 surface a flash toast, not an alert or an error-level\n // stack dump that reads like an unhandled exception.\n const message = err instanceof Error && err.message ? err.message : errorMessage\n logger.warn('Index action failed', { message })\n flash(message, 'error')\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [qc, t, runMutation, retryLastMutation],\n )\n\n const triggerVector = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.vectorPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.vectorPurge')\n : t('query_index.table.actions.vectorReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow('/api/search/embeddings/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n entityId,\n purgeFirst: action === 'purge',\n }),\n }, { errorMessage }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'query_index.vector',\n resourceId: entityId,\n retryLastMutation,\n },\n mutationPayload: { action, entityId, purgeFirst: action === 'purge' },\n })\n } catch (err) {\n const message = err instanceof Error && err.message ? err.message : errorMessage\n logger.warn('Vector action failed', { message })\n flash(message, 'error')\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t, runMutation, retryLastMutation],\n )\n\n const triggerFulltext = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.fulltextPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.fulltextPurge')\n : t('query_index.table.actions.fulltextReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow('/api/search/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n action: action === 'purge' ? 'clear' : 'reindex',\n entityId,\n }),\n }, { errorMessage }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'query_index.fulltext',\n resourceId: entityId,\n retryLastMutation,\n },\n mutationPayload: { action, entityId },\n })\n } catch (err) {\n const message = err instanceof Error && err.message ? err.message : errorMessage\n logger.warn('Fulltext action failed', { message })\n flash(message, 'error')\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t, runMutation, retryLastMutation],\n )\n\n return (\n <>\n <DataTable\n title={t('query_index.nav.queryIndexes')}\n actions={(\n <>\n <Button\n variant=\"outline\"\n onClick={() => {\n setRefreshSeq((v) => v + 1)\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n }}\n >\n {t('query_index.table.refresh')}\n </Button>\n </>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('query_index.table.searchPlaceholder')}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n perspective={{ tableId: 'query_index.status.list' }}\n rowActions={(row) => {\n const items: Array<{ id: string; label: string; onSelect: () => void; destructive?: boolean }> = [\n { id: 'reindex', label: t('query_index.table.actions.reindex'), onSelect: () => void trigger('reindex', row.entityId) },\n {\n id: 'reindex-force',\n label: t('query_index.table.actions.reindexForce'),\n onSelect: () => void trigger('reindex', row.entityId, { force: true }),\n },\n {\n id: 'purge',\n label: t('query_index.table.actions.purge'),\n destructive: true,\n onSelect: () => void trigger('purge', row.entityId),\n },\n ]\n\n if (row.vectorEnabled) {\n items.push(\n {\n id: 'vector-reindex',\n label: t('query_index.table.actions.vectorReindex'),\n onSelect: () => void triggerVector('reindex', row.entityId),\n },\n {\n id: 'vector-purge',\n label: t('query_index.table.actions.vectorPurge'),\n destructive: true,\n onSelect: () => void triggerVector('purge', row.entityId),\n },\n )\n }\n\n if (row.fulltextEnabled) {\n items.push(\n {\n id: 'fulltext-reindex',\n label: t('query_index.table.actions.fulltextReindex'),\n onSelect: () => void triggerFulltext('reindex', row.entityId),\n },\n {\n id: 'fulltext-purge',\n label: t('query_index.table.actions.fulltextPurge'),\n destructive: true,\n onSelect: () => void triggerFulltext('purge', row.entityId),\n },\n )\n }\n\n return <RowActions items={items} />\n }}\n pagination={{ page, pageSize: 50, total: rows.length, totalPages: 1, onPageChange: setPage }}\n isLoading={isLoading}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('query_index').child({ component: 'indexes-table' })\n\nconst MUTATION_CONTEXT_ID = 'query_index.status.list:actions'\n\ntype Translator = (key: string, params?: Record<string, string | number>) => string\n\ntype PartitionStatus = {\n partitionIndex: number | null\n partitionCount: number | null\n status: 'reindexing' | 'purging' | 'stalled' | 'completed' | 'failed'\n processedCount?: number | null\n totalCount?: number | null\n heartbeatAt?: string | null\n startedAt?: string | null\n finishedAt?: string | null\n}\n\ntype JobStatus = {\n status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed'\n startedAt?: string | null\n finishedAt?: string | null\n heartbeatAt?: string | null\n processedCount?: number | null\n totalCount?: number | null\n partitions?: PartitionStatus[]\n scope?: {\n status?: 'reindexing' | 'purging' | 'stalled' | 'completed' | 'failed' | null\n processedCount?: number | null\n totalCount?: number | null\n } | null\n}\n\ntype Row = {\n entityId: string\n label: string\n baseCount: number | null\n indexCount: number | null\n vectorCount: number | null\n /** The entity is configured for vector search (declares `buildSource`). */\n vectorEnabled: boolean\n /** Vector indexing can actually run \u2014 provider reachable and auto-indexing on. */\n vectorIndexingActive?: boolean\n fulltextCount: number | null\n fulltextEnabled: boolean\n hasCustomFields?: boolean\n /** Legacy aggregate: query index AND vector coverage. Preserved for API consumers. */\n ok: boolean\n /** Query index alone is in sync with the base table \u2014 the signal this page reports. */\n queryIndexOk?: boolean\n job?: JobStatus\n}\n\ntype Resp = { items: Row[] }\n\nconst BACKEND_FILTER_ID = 'backend'\n// Kept at or below the repository-wide 100-row cap; the entity list grows with every\n// installed module, so it cannot be allowed to scale with the row count.\nconst PAGE_SIZE = 50\n\n/** Sort key for a column id. Mirrors the column `accessorFn`s so paging and sorting agree. */\nfunction sortValue(row: Row, columnId: string): string | number {\n if (columnId === 'entityId') return row.entityId\n if (columnId === 'baseCount') return row.baseCount ?? 0\n if (columnId === 'indexCount') return row.indexCount ?? 0\n if (columnId === 'vectorCount') return isVectorReportable(row) ? row.vectorCount ?? 0 : -1\n if (columnId === 'fulltextCount') return row.fulltextEnabled ? row.fulltextCount ?? 0 : -1\n return row.entityId\n}\n\ntype BackendFilter = 'vector' | 'fulltext' | 'custom_fields'\n\n/**\n * Vector coverage is only worth showing when embeddings can actually be written; otherwise\n * the count is a permanent 0 that reads as a gap. Older payloads without the flag are\n * treated as active so the column keeps working against an un-upgraded server.\n */\nfunction isVectorReportable(row: Row): boolean {\n return row.vectorEnabled && (row.vectorIndexingActive ?? true)\n}\n\nfunction matchesBackendFilter(row: Row, backend: BackendFilter): boolean {\n if (backend === 'vector') return row.vectorEnabled\n if (backend === 'fulltext') return row.fulltextEnabled\n return Boolean(row.hasCustomFields)\n}\n\nfunction formatCount(value: number | null): string {\n if (value == null) return '\u2014'\n return value.toLocaleString()\n}\n\nfunction formatNumeric(value: number | null | undefined): string | null {\n if (value == null) return null\n return Number(value).toLocaleString()\n}\n\nfunction formatProgressLabel(\n processed: number | null | undefined,\n total: number | null | undefined,\n t: Translator,\n): string | null {\n const processedText = formatNumeric(processed)\n if (!processedText) return null\n const totalText = formatNumeric(total)\n if (totalText) return t('query_index.table.status.progress', { processed: processedText, total: totalText })\n return t('query_index.table.status.progressSingle', { processed: processedText })\n}\n\nfunction translateJobStatus(\n t: Translator,\n status: JobStatus['status'] | undefined,\n ok: boolean,\n measured: boolean,\n): string {\n const idleLabel = () => {\n // No coverage snapshot yet \u2014 an async refresh is pending, so the index is not known to\n // be out of sync. Saying so would be the same false alarm this page used to raise.\n if (!measured) return t('query_index.table.status.not_measured')\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n }\n if (!status || status === 'idle') return idleLabel()\n if (status === 'reindexing') return t('query_index.table.status.reindexing')\n if (status === 'purging') return t('query_index.table.status.purging')\n if (status === 'stalled') return t('query_index.table.status.stalled')\n if (status === 'failed') return t('query_index.table.status.failed')\n return idleLabel()\n}\n\nfunction translateScopeStatus(\n t: Translator,\n status: PartitionStatus['status'] | JobStatus['status'] | undefined | null,\n): string {\n if (status === 'reindexing') return t('query_index.table.status.scope.reindexing')\n if (status === 'purging') return t('query_index.table.status.scope.purging')\n if (status === 'stalled') return t('query_index.table.status.scope.stalled')\n if (status === 'failed') return t('query_index.table.status.scope.failed')\n return t('query_index.table.status.scope.completed')\n}\n\n\nfunction createColumns(t: Translator): ColumnDef<Row>[] {\n return [\n {\n id: 'entityId',\n header: () => t('query_index.table.columns.entity'),\n accessorKey: 'entityId',\n // The entity id is the only identifier on this page and ids run to ~45 characters\n // (`catalog:catalog_product_category_assignment`). DataTable's default 150px clamp\n // ellipsised them into indistinguishable `customers:customer\u2026` rows.\n meta: { priority: 1, maxWidth: '340px' },\n },\n {\n id: 'baseCount',\n header: () => t('query_index.table.columns.records'),\n accessorFn: (row) => row.baseCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.baseCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'indexCount',\n header: () => t('query_index.table.columns.indexed'),\n accessorFn: (row) => row.indexCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.indexCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'vectorCount',\n header: () => t('query_index.table.columns.vector'),\n accessorFn: (row) => (isVectorReportable(row) ? row.vectorCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!isVectorReportable(record)) return <span>\u2014</span>\n const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount\n const display = formatCount(record.vectorCount)\n const className = ok ? 'text-status-success-text' : 'text-status-warning-text'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'fulltextCount',\n header: () => t('query_index.table.columns.fulltext'),\n accessorFn: (row) => (row.fulltextEnabled ? row.fulltextCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.fulltextEnabled) return <span>\u2014</span>\n const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount\n const display = formatCount(record.fulltextCount)\n const className = ok ? 'text-status-success-text' : 'text-status-warning-text'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'status',\n header: () => t('query_index.table.columns.status'),\n cell: ({ row }) => {\n const record = row.original\n const job = record.job\n const partitions = job?.partitions ?? []\n const measured = record.baseCount != null || record.indexCount != null\n\n // A non-partitioned reindex \u2014 and every purge, which never partitions\n // (`lib/purge.ts` always passes `partitionIndex: null`) \u2014 is tracked as a scope row.\n // The server derives the top-level status from partition rows only, so it reports\n // `idle` for those runs; without folding the scope row in, the default reindex and\n // every purge would render as though nothing were happening.\n const scopeStatus = job?.scope?.status ?? null\n const scopeInFlight = scopeStatus != null && scopeStatus !== 'completed'\n const effectiveStatus: JobStatus['status'] | undefined = job\n ? (job.status !== 'idle'\n ? job.status\n : (scopeInFlight ? (scopeStatus as JobStatus['status']) : 'idle'))\n : undefined\n\n const indexOk = record.queryIndexOk ?? record.ok\n const ok = indexOk && (!job || effectiveStatus === 'idle')\n const jobFailed = effectiveStatus === 'failed'\n const statusText = translateJobStatus(t, effectiveStatus, ok, measured)\n\n const jobInFlight = Boolean(effectiveStatus && effectiveStatus !== 'idle')\n || partitions.some((part) => part.status !== 'completed')\n\n // Job counters and index coverage are different numbers: the counters describe the\n // last reindex run, the Records/Indexed columns describe the index right now. While\n // a job runs, \"Reindexing (5/14)\" is the only place progress is visible. Once it is\n // idle the counters are stale and duplicate the columns \u2014 worse, they can contradict\n // them: an entity whose last run processed 14 rows but landed 2 in the index rendered\n // \"Out of sync (14/14)\" next to Records 14 / Indexed 2.\n const jobProgress = jobInFlight && job\n ? formatProgressLabel(job.processedCount ?? null, job.totalCount ?? null, t)\n : null\n const label = jobProgress\n ? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })\n : statusText\n let variant: StatusBadgeVariant = 'neutral'\n if (job) {\n if (effectiveStatus === 'stalled' || jobFailed) variant = 'error'\n else if (effectiveStatus === 'reindexing' || effectiveStatus === 'purging') variant = 'warning'\n else variant = ok ? 'success' : 'neutral'\n } else {\n variant = ok ? 'success' : 'neutral'\n }\n\n const lines: string[] = []\n\n // Per-partition detail earns its space only while work is in flight \u2014 that is when\n // it tells you which partition is running or stuck. Once the job is idle and every\n // partition is finished, each line just restates the badge, on every row.\n if (jobInFlight) {\n if (job?.scope && partitions.length <= 1) {\n const scopeStatusText = translateScopeStatus(t, scopeStatus)\n const scopeProgress = formatProgressLabel(job.scope.processedCount ?? null, job.scope.totalCount ?? null, t)\n const scopeLabel = t('query_index.table.status.scopeLabel')\n lines.push(`${scopeLabel}: ${scopeStatusText}${scopeProgress ? ` (${scopeProgress})` : ''}`)\n }\n\n if (partitions.length > 1) {\n for (const part of partitions) {\n const partitionLabel =\n part.partitionIndex != null\n ? t('query_index.table.status.partitionLabel', { index: Number(part.partitionIndex) + 1 })\n : t('query_index.table.status.scopeLabel')\n const partitionStatus = translateScopeStatus(t, part.status)\n const partitionProgress = formatProgressLabel(part.processedCount ?? null, part.totalCount ?? null, t)\n lines.push(`${partitionLabel}: ${partitionStatus}${partitionProgress ? ` (${partitionProgress})` : ''}`)\n }\n }\n }\n\n return (\n <div className=\"space-y-1.5\">\n <div>\n <StatusBadge variant={variant} dot>\n {label}\n </StatusBadge>\n </div>\n {lines.length > 0 && (\n <div className=\"text-xs text-muted-foreground\">\n {lines.map((line, idx) => (\n <div key={idx}>{line}</div>\n ))}\n </div>\n )}\n </div>\n )\n },\n meta: { priority: 1 },\n },\n ]\n}\n\nexport default function QueryIndexesTable() {\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'entityId', desc: false }])\n const [search, setSearch] = React.useState('')\n const [page, setPage] = React.useState(1)\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const qc = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [refreshSeq, setRefreshSeq] = React.useState(0)\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const columns = React.useMemo(() => createColumns(t), [t])\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: BACKEND_FILTER_ID,\n label: t('query_index.table.filters.backend'),\n type: 'select',\n options: [\n { label: t('query_index.table.filters.backendOptions.vector'), value: 'vector' },\n { label: t('query_index.table.filters.backendOptions.fulltext'), value: 'fulltext' },\n { label: t('query_index.table.filters.backendOptions.customFields'), value: 'custom_fields' },\n ],\n },\n ], [t])\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: MUTATION_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const { data, isLoading } = useQuery<Resp>({\n queryKey: ['query-index-status', scopeVersion, refreshSeq],\n queryFn: async () => {\n const baseUrl = '/api/query_index/status'\n const url = refreshSeq > 0 ? `${baseUrl}?refresh=${refreshSeq}` : baseUrl\n return readApiResultOrThrow<Resp>(\n url,\n undefined,\n { errorMessage: t('query_index.table.errors.loadFailed') },\n )\n },\n refetchInterval: 4000,\n })\n\n const rowsAll = data?.items || []\n const matchedRows = React.useMemo(() => {\n const backend = filterValues[BACKEND_FILTER_ID] as BackendFilter | undefined\n const q = search.trim().toLowerCase()\n if (!q && !backend) return rowsAll\n return rowsAll.filter((row) => {\n if (q && !row.entityId.toLowerCase().includes(q)) return false\n if (backend && !matchesBackendFilter(row, backend)) return false\n return true\n })\n }, [rowsAll, search, filterValues])\n\n // The endpoint returns every entity in one payload, so paging is ours to do. Sorting has\n // to happen here rather than in the table: slicing first would let DataTable sort only the\n // visible page, so page 2 would not hold the rows that sort into it.\n const sortedRows = React.useMemo(() => {\n const sort = sorting[0]\n if (!sort) return matchedRows\n const direction = sort.desc ? -1 : 1\n return [...matchedRows].sort((a, b) => {\n const left = sortValue(a, sort.id)\n const right = sortValue(b, sort.id)\n if (typeof left === 'string' || typeof right === 'string') {\n return String(left).localeCompare(String(right)) * direction\n }\n return (Number(left) - Number(right)) * direction\n })\n }, [matchedRows, sorting])\n\n const total = sortedRows.length\n const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))\n const currentPage = Math.min(page, totalPages)\n const rows = React.useMemo(\n () => sortedRows.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE),\n [sortedRows, currentPage],\n )\n\n const pagination = React.useMemo(() => ({\n page: currentPage,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }), [currentPage, total, totalPages])\n\n const trigger = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string, opts?: { force?: boolean }) => {\n const body: Record<string, unknown> = { entityType: entityId }\n if (opts?.force) body.force = true\n const actionLabel =\n action === 'purge' ? t('query_index.table.actions.purge') : t('query_index.table.actions.reindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow(`/api/query_index/${action}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n }, { errorMessage }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'query_index',\n resourceId: entityId,\n retryLastMutation,\n },\n mutationPayload: { action, entityType: entityId, force: Boolean(opts?.force) },\n })\n } catch (err) {\n // Expected operational failures (e.g. a 503 when a search backend is not\n // configured) \u2014 surface a flash toast, not an alert or an error-level\n // stack dump that reads like an unhandled exception.\n const message = err instanceof Error && err.message ? err.message : errorMessage\n logger.warn('Index action failed', { message })\n flash(message, 'error')\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [qc, t, runMutation, retryLastMutation],\n )\n\n const triggerVector = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.vectorPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.vectorPurge')\n : t('query_index.table.actions.vectorReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow('/api/search/embeddings/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n entityId,\n purgeFirst: action === 'purge',\n }),\n }, { errorMessage }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'query_index.vector',\n resourceId: entityId,\n retryLastMutation,\n },\n mutationPayload: { action, entityId, purgeFirst: action === 'purge' },\n })\n } catch (err) {\n const message = err instanceof Error && err.message ? err.message : errorMessage\n logger.warn('Vector action failed', { message })\n flash(message, 'error')\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t, runMutation, retryLastMutation],\n )\n\n const triggerFulltext = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.fulltextPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.fulltextPurge')\n : t('query_index.table.actions.fulltextReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow('/api/search/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n action: action === 'purge' ? 'clear' : 'reindex',\n entityId,\n }),\n }, { errorMessage }),\n context: {\n formId: MUTATION_CONTEXT_ID,\n resourceKind: 'query_index.fulltext',\n resourceId: entityId,\n retryLastMutation,\n },\n mutationPayload: { action, entityId },\n })\n } catch (err) {\n const message = err instanceof Error && err.message ? err.message : errorMessage\n logger.warn('Fulltext action failed', { message })\n flash(message, 'error')\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t, runMutation, retryLastMutation],\n )\n\n return (\n <>\n <DataTable\n title={t('query_index.nav.queryIndexes')}\n actions={(\n <>\n <Button\n variant=\"outline\"\n onClick={() => {\n setRefreshSeq((v) => v + 1)\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n }}\n >\n {t('query_index.table.refresh')}\n </Button>\n </>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('query_index.table.searchPlaceholder')}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => {\n setFilterValues(values)\n setPage(1)\n }}\n onFiltersClear={() => {\n setFilterValues({})\n setPage(1)\n }}\n sortable\n manualSorting\n sorting={sorting}\n onSortingChange={(next) => {\n setSorting(next)\n setPage(1)\n }}\n perspective={{ tableId: 'query_index.status.list' }}\n rowActions={(row) => {\n const items: Array<{ id: string; label: string; onSelect: () => void; destructive?: boolean }> = [\n { id: 'reindex', label: t('query_index.table.actions.reindex'), onSelect: () => void trigger('reindex', row.entityId) },\n {\n id: 'reindex-force',\n label: t('query_index.table.actions.reindexForce'),\n onSelect: () => void trigger('reindex', row.entityId, { force: true }),\n },\n {\n id: 'purge',\n label: t('query_index.table.actions.purge'),\n destructive: true,\n onSelect: () => void trigger('purge', row.entityId),\n },\n ]\n\n // Manual vector actions stay available whenever the entity declares `buildSource`,\n // even when auto-indexing is off \u2014 a manual reindex is still a valid operation.\n if (row.vectorEnabled) {\n items.push(\n {\n id: 'vector-reindex',\n label: t('query_index.table.actions.vectorReindex'),\n onSelect: () => void triggerVector('reindex', row.entityId),\n },\n {\n id: 'vector-purge',\n label: t('query_index.table.actions.vectorPurge'),\n destructive: true,\n onSelect: () => void triggerVector('purge', row.entityId),\n },\n )\n }\n\n if (row.fulltextEnabled) {\n items.push(\n {\n id: 'fulltext-reindex',\n label: t('query_index.table.actions.fulltextReindex'),\n onSelect: () => void triggerFulltext('reindex', row.entityId),\n },\n {\n id: 'fulltext-purge',\n label: t('query_index.table.actions.fulltextPurge'),\n destructive: true,\n onSelect: () => void triggerFulltext('purge', row.entityId),\n },\n )\n }\n\n return <RowActions items={items} />\n }}\n pagination={pagination}\n isLoading={isLoading}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import { isUniqueViolation } from "@open-mercato/shared/lib/db/pg-errors";
|
|
|
4
4
|
import { buildIndexDocument } from "./document.js";
|
|
5
5
|
import { replaceSearchTokensForBatch, isSearchDebugEnabled } from "./search-tokens.js";
|
|
6
6
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
|
+
import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
|
|
7
8
|
const logger = createLogger("query_index").child({ component: "reindex-batch" });
|
|
8
9
|
const MAX_RECORDED_ROW_ERRORS_PER_BATCH = 50;
|
|
9
10
|
class QueryIndexBatchWriteError extends Error {
|
|
@@ -110,6 +111,7 @@ async function upsertIndexBatch(db, entityType, rows, scope, options = {}) {
|
|
|
110
111
|
}
|
|
111
112
|
const basePayloads = [];
|
|
112
113
|
const debugEnabled = isSearchDebugEnabled();
|
|
114
|
+
const searchConfig = resolveSearchConfig();
|
|
113
115
|
for (const row of rows) {
|
|
114
116
|
const recordId = normalizeId(row.id);
|
|
115
117
|
const baseOrg = normalizeScopedValue(row.organization_id);
|
|
@@ -132,10 +134,15 @@ async function upsertIndexBatch(db, entityType, rows, scope, options = {}) {
|
|
|
132
134
|
if (!entityRow) return row;
|
|
133
135
|
return { ...entityRow, ...row };
|
|
134
136
|
})();
|
|
135
|
-
let doc = buildIndexDocument(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
let doc = buildIndexDocument(
|
|
138
|
+
mergedRow,
|
|
139
|
+
values,
|
|
140
|
+
{
|
|
141
|
+
organizationId: scopeOrg ?? null,
|
|
142
|
+
tenantId: scopeTenant ?? null
|
|
143
|
+
},
|
|
144
|
+
{ entityType, config: searchConfig }
|
|
145
|
+
);
|
|
139
146
|
let tokenDoc = doc;
|
|
140
147
|
if (typeof options.encryptDoc === "function") {
|
|
141
148
|
try {
|
|
@@ -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'\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\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 (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": "AAAA,SAAsB,WAAW;AACjC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,0BAAsD;AAC/D,SAAS,6BAA6B,4BAA4B;AAClE,SAAS,oBAAoB;
|
|
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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",
|
|
6
6
|
"names": ["written"]
|
|
7
7
|
}
|