@open-mercato/core 0.6.7-develop.6706.1.b3a4c759bb → 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.
Files changed (111) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/admin/nav.js +14 -4
  3. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +8 -19
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/lib/backendChrome.js +31 -14
  7. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  8. package/dist/modules/customers/api/utils.js +12 -18
  9. package/dist/modules/customers/api/utils.js.map +2 -2
  10. package/dist/modules/customers/cli.js +11 -4
  11. package/dist/modules/customers/cli.js.map +2 -2
  12. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
  13. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  14. package/dist/modules/customers/components/formConfig.js +14 -10
  15. package/dist/modules/customers/components/formConfig.js.map +2 -2
  16. package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
  17. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
  18. package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
  19. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  20. package/dist/modules/dashboards/lib/aggregations.js +76 -40
  21. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  22. package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
  23. package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js +228 -5
  25. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  26. package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
  27. package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
  28. package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
  29. package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
  30. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
  31. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
  32. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
  33. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
  34. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
  35. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
  36. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
  37. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
  38. package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
  39. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  40. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
  41. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
  42. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
  43. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
  44. package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
  45. package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
  46. package/dist/modules/messages/lib/searchLookup.js +8 -16
  47. package/dist/modules/messages/lib/searchLookup.js.map +2 -2
  48. package/dist/modules/query_index/api/openapi.js +13 -1
  49. package/dist/modules/query_index/api/openapi.js.map +2 -2
  50. package/dist/modules/query_index/api/status.js +165 -105
  51. package/dist/modules/query_index/api/status.js.map +2 -2
  52. package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
  53. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  54. package/dist/modules/query_index/lib/batch.js +11 -4
  55. package/dist/modules/query_index/lib/batch.js.map +2 -2
  56. package/dist/modules/query_index/lib/document.js +12 -5
  57. package/dist/modules/query_index/lib/document.js.map +2 -2
  58. package/dist/modules/query_index/lib/engine.js +36 -4
  59. package/dist/modules/query_index/lib/engine.js.map +2 -2
  60. package/dist/modules/query_index/lib/indexer.js +1 -1
  61. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  62. package/dist/modules/query_index/lib/search-tokens.js +7 -4
  63. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  64. package/dist/modules/sales/api/documents/factory.js +14 -3
  65. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  66. package/dist/modules/sales/commands/documents.js +22 -1
  67. package/dist/modules/sales/commands/documents.js.map +2 -2
  68. package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
  69. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
  70. package/dist/modules/sales/data/validators.js +25 -4
  71. package/dist/modules/sales/data/validators.js.map +2 -2
  72. package/package.json +7 -7
  73. package/src/modules/auth/api/admin/nav.ts +24 -4
  74. package/src/modules/auth/api/users/route.ts +8 -28
  75. package/src/modules/auth/lib/backendChrome.tsx +51 -13
  76. package/src/modules/customers/api/utils.ts +12 -31
  77. package/src/modules/customers/cli.ts +11 -4
  78. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
  79. package/src/modules/customers/components/formConfig.tsx +12 -8
  80. package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
  81. package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
  82. package/src/modules/dashboards/lib/aggregations.ts +122 -43
  83. package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
  84. package/src/modules/dashboards/services/widgetDataService.ts +342 -4
  85. package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
  86. package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
  87. package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
  88. package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
  89. package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
  90. package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
  91. package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
  92. package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
  93. package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
  94. package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
  95. package/src/modules/messages/lib/searchLookup.ts +11 -34
  96. package/src/modules/query_index/api/openapi.ts +12 -0
  97. package/src/modules/query_index/api/status.ts +238 -136
  98. package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
  99. package/src/modules/query_index/i18n/de.json +5 -3
  100. package/src/modules/query_index/i18n/en.json +5 -3
  101. package/src/modules/query_index/i18n/es.json +5 -3
  102. package/src/modules/query_index/i18n/pl.json +5 -3
  103. package/src/modules/query_index/lib/batch.ts +11 -4
  104. package/src/modules/query_index/lib/document.ts +35 -4
  105. package/src/modules/query_index/lib/engine.ts +53 -5
  106. package/src/modules/query_index/lib/indexer.ts +1 -1
  107. package/src/modules/query_index/lib/search-tokens.ts +13 -4
  108. package/src/modules/sales/api/documents/factory.ts +28 -3
  109. package/src/modules/sales/commands/documents.ts +26 -2
  110. package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
  111. package/src/modules/sales/data/validators.ts +50 -5
@@ -7,6 +7,11 @@ import { readCoverageSnapshots, refreshCoverageSnapshot } from "../lib/coverage.
7
7
  import { mapWithConcurrency } from "@open-mercato/shared/lib/query/bounded-decrypt";
8
8
  import { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from "./openapi.js";
9
9
  import { flattenSystemEntityIds } from "@open-mercato/shared/lib/entities/system-entities";
10
+ import {
11
+ envDisablesAutoIndexing,
12
+ SEARCH_AUTO_INDEX_CONFIG_KEY,
13
+ SEARCH_AUTO_INDEX_CONFIG_MODULE
14
+ } from "@open-mercato/shared/lib/search/auto-indexing";
10
15
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
11
16
  const metadata = {
12
17
  GET: { requireAuth: true, requireFeatures: ["query_index.status.view"] }
@@ -63,35 +68,61 @@ async function GET(req) {
63
68
  const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }));
64
69
  const byId = /* @__PURE__ */ new Map();
65
70
  for (const g of generated) byId.set(g.entityId, g);
66
- let entityIds = generatedIds.slice();
67
71
  let searchModuleConfigs = [];
68
72
  try {
69
73
  searchModuleConfigs = container.resolve("searchModuleConfigs");
70
74
  } catch {
71
75
  }
72
- const vectorEnabledEntities = /* @__PURE__ */ new Set();
76
+ const vectorConfiguredEntities = /* @__PURE__ */ new Set();
73
77
  const fulltextEnabledEntities = /* @__PURE__ */ new Set();
78
+ const searchConfiguredEntities = /* @__PURE__ */ new Set();
74
79
  for (const moduleConfig of searchModuleConfigs) {
75
80
  for (const entity of moduleConfig.entities ?? []) {
76
81
  if (entity.enabled !== false) {
77
82
  if (typeof entity.buildSource === "function") {
78
- vectorEnabledEntities.add(entity.entityId);
83
+ vectorConfiguredEntities.add(entity.entityId);
84
+ searchConfiguredEntities.add(entity.entityId);
79
85
  }
80
86
  if (entity.fieldPolicy && typeof entity.fieldPolicy === "object") {
81
87
  fulltextEnabledEntities.add(entity.entityId);
88
+ searchConfiguredEntities.add(entity.entityId);
82
89
  }
83
90
  }
84
91
  }
85
92
  }
86
- let fulltextStrategy = null;
93
+ let searchStrategies = [];
87
94
  try {
88
- const searchStrategies = container.resolve("searchStrategies");
89
- fulltextStrategy = searchStrategies?.find(
90
- (s) => s?.id === "fulltext"
91
- ) ?? null;
95
+ searchStrategies = container.resolve("searchStrategies") ?? [];
92
96
  } catch {
93
- fulltextStrategy = null;
97
+ searchStrategies = [];
94
98
  }
99
+ const fulltextStrategy = searchStrategies.find(
100
+ (s) => s?.id === "fulltext"
101
+ ) ?? null;
102
+ const vectorRuntimeEnabled = await (async () => {
103
+ if (vectorConfiguredEntities.size === 0) return false;
104
+ if (envDisablesAutoIndexing()) return false;
105
+ const vectorStrategy = searchStrategies.find(
106
+ (s) => s?.id === "vector"
107
+ );
108
+ if (typeof vectorStrategy?.isAvailable !== "function") return false;
109
+ try {
110
+ if (!await vectorStrategy.isAvailable()) return false;
111
+ } catch {
112
+ return false;
113
+ }
114
+ try {
115
+ const moduleConfigService = container.resolve("moduleConfigService");
116
+ const value = await moduleConfigService.getValue(
117
+ SEARCH_AUTO_INDEX_CONFIG_MODULE,
118
+ SEARCH_AUTO_INDEX_CONFIG_KEY,
119
+ { defaultValue: true, scope: { tenantId } }
120
+ );
121
+ return value !== false;
122
+ } catch {
123
+ return true;
124
+ }
125
+ })();
95
126
  let fulltextEntityCounts = null;
96
127
  if (fulltextStrategy) {
97
128
  try {
@@ -100,6 +131,7 @@ async function GET(req) {
100
131
  fulltextEntityCounts = null;
101
132
  }
102
133
  }
134
+ const customFieldEntities = /* @__PURE__ */ new Set();
103
135
  try {
104
136
  let cfQuery = db.selectFrom("custom_field_defs").select(["entity_id"]).distinct().where("is_active", "=", true);
105
137
  if (tenantId != null) {
@@ -117,16 +149,18 @@ async function GET(req) {
117
149
  ]));
118
150
  }
119
151
  const cfRows = await cfQuery.execute();
120
- const enabled = new Set((cfRows || []).map((r) => String(r.entity_id)));
121
- entityIds = entityIds.filter((id) => enabled.has(id));
152
+ for (const row of cfRows || []) customFieldEntities.add(String(row.entity_id));
122
153
  } catch {
123
154
  }
124
155
  const HEARTBEAT_STALE_MS = 6e4;
125
156
  const COVERAGE_STALE_MS = 6e4;
126
157
  const COVERAGE_REFRESH_CONCURRENCY = 8;
127
- async function fetchJobSummary(entityType, tenantIdParam, organizationIdParam) {
158
+ const idleJobSummary = () => ({ status: "idle", partitions: [] });
159
+ async function fetchJobSummaries(entityTypes, tenantIdParam, organizationIdParam) {
160
+ const byEntity = /* @__PURE__ */ new Map();
161
+ if (!entityTypes.length) return byEntity;
128
162
  try {
129
- let jobQuery = db.selectFrom("entity_index_jobs").selectAll().where("entity_type", "=", entityType).where(sql`tenant_id is not distinct from ${tenantIdParam ?? null}`);
163
+ let jobQuery = db.selectFrom("entity_index_jobs").selectAll().where("entity_type", "in", entityTypes).where(sql`tenant_id is not distinct from ${tenantIdParam ?? null}`);
130
164
  if (organizationIdParam != null) {
131
165
  jobQuery = jobQuery.where((eb) => eb.or([
132
166
  eb("organization_id", "=", organizationIdParam),
@@ -136,99 +170,113 @@ async function GET(req) {
136
170
  jobQuery = jobQuery.where(sql`organization_id is not distinct from ${null}`);
137
171
  }
138
172
  const rows = await jobQuery.orderBy("started_at", "desc").execute();
139
- if (!rows.length) {
140
- return { status: "idle", partitions: [] };
141
- }
142
- const preferOrg = organizationIdParam != null && rows.some((row) => row.organization_id === organizationIdParam);
143
- const pickPreferred = (existing, candidate) => {
144
- if (!existing) return candidate;
145
- if (preferOrg) {
146
- if (candidate.orgMatch && !existing.orgMatch) return candidate;
147
- if (!candidate.orgMatch && existing.orgMatch) return existing;
148
- }
149
- if (candidate.tenantMatch && !existing.tenantMatch) return candidate;
150
- if (!candidate.tenantMatch && existing.tenantMatch) return existing;
151
- return candidate.startedTs > existing.startedTs ? candidate : existing;
152
- };
153
- const partitionRows = /* @__PURE__ */ new Map();
154
- let scopeRow = null;
173
+ const rowsByEntity = /* @__PURE__ */ new Map();
155
174
  for (const row of rows) {
156
- const key = String(row.partition_index ?? "__null__");
157
- const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0;
158
- const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true;
159
- const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null;
160
- const candidate = { row, startedTs, tenantMatch, orgMatch };
161
- if (row.partition_index == null) {
162
- scopeRow = pickPreferred(scopeRow, candidate);
163
- continue;
164
- }
165
- const existing = partitionRows.get(key);
166
- partitionRows.set(key, pickPreferred(existing ?? null, candidate));
175
+ const entityType = String(row.entity_type ?? "");
176
+ if (!entityType) continue;
177
+ const bucket = rowsByEntity.get(entityType);
178
+ if (bucket) bucket.push(row);
179
+ else rowsByEntity.set(entityType, [row]);
167
180
  }
168
- const partitions = Array.from(partitionRows.values()).filter((entry) => !preferOrg || entry.orgMatch).map(({ row }) => {
169
- const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null;
170
- const startedDate = row.started_at ? new Date(row.started_at) : null;
171
- const finishedDate = row.finished_at ? new Date(row.finished_at) : null;
172
- const stalled = !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS);
173
- const state = finishedDate ? row.status === "failed" ? "failed" : "completed" : stalled ? "stalled" : row.status || "reindexing";
174
- return {
175
- partitionIndex: row.partition_index ?? null,
176
- partitionCount: row.partition_count ?? null,
177
- status: state,
178
- startedAt: startedDate ? startedDate.toISOString() : null,
179
- finishedAt: finishedDate ? finishedDate.toISOString() : null,
180
- heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,
181
- processedCount: row.processed_count ?? null,
182
- totalCount: row.total_count ?? null
183
- };
184
- }).sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0));
185
- const activePartitions = partitions.filter((p) => !p.finishedAt);
186
- const runningPartitions = activePartitions.filter(
187
- (p) => p.status === "reindexing" || p.status === "purging"
188
- );
189
- const stalledPartitions = activePartitions.filter((p) => p.status === "stalled");
190
- const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null;
191
- let status = "idle";
192
- if (activePartitions.length) {
193
- if (runningPartitions.length) {
194
- status = runningPartitions.some((p) => p.status === "purging") ? "purging" : "reindexing";
195
- } else if (stalledPartitions.length) {
196
- status = "stalled";
197
- }
198
- } else if (partitions.some((p) => p.status === "failed") || scopeCandidate?.row.finished_at && scopeCandidate.row.status === "failed") {
199
- status = "failed";
181
+ for (const [entityType, entityRows] of rowsByEntity) {
182
+ byEntity.set(entityType, buildJobSummary(entityRows, organizationIdParam, tenantIdParam));
200
183
  }
201
- const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null;
202
- const finishedAt = status === "idle" || status === "failed" ? partitions.find((p) => p.finishedAt)?.finishedAt ?? null : null;
203
- const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null;
204
- const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0);
205
- const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0);
206
- const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null;
184
+ } catch {
185
+ return byEntity;
186
+ }
187
+ return byEntity;
188
+ }
189
+ function buildJobSummary(rows, organizationIdParam, tenantIdParam) {
190
+ if (!rows.length) {
191
+ return idleJobSummary();
192
+ }
193
+ const preferOrg = organizationIdParam != null && rows.some((row) => row.organization_id === organizationIdParam);
194
+ const pickPreferred = (existing, candidate) => {
195
+ if (!existing) return candidate;
196
+ if (preferOrg) {
197
+ if (candidate.orgMatch && !existing.orgMatch) return candidate;
198
+ if (!candidate.orgMatch && existing.orgMatch) return existing;
199
+ }
200
+ if (candidate.tenantMatch && !existing.tenantMatch) return candidate;
201
+ if (!candidate.tenantMatch && existing.tenantMatch) return existing;
202
+ return candidate.startedTs > existing.startedTs ? candidate : existing;
203
+ };
204
+ const partitionRows = /* @__PURE__ */ new Map();
205
+ let scopeRow = null;
206
+ for (const row of rows) {
207
+ const key = String(row.partition_index ?? "__null__");
208
+ const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0;
209
+ const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true;
210
+ const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null;
211
+ const candidate = { row, startedTs, tenantMatch, orgMatch };
212
+ if (row.partition_index == null) {
213
+ scopeRow = pickPreferred(scopeRow, candidate);
214
+ continue;
215
+ }
216
+ const existing = partitionRows.get(key);
217
+ partitionRows.set(key, pickPreferred(existing ?? null, candidate));
218
+ }
219
+ const partitions = Array.from(partitionRows.values()).filter((entry) => !preferOrg || entry.orgMatch).map(({ row }) => {
220
+ const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null;
221
+ const startedDate = row.started_at ? new Date(row.started_at) : null;
222
+ const finishedDate = row.finished_at ? new Date(row.finished_at) : null;
223
+ const stalled = !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS);
224
+ const state = finishedDate ? row.status === "failed" ? "failed" : "completed" : stalled ? "stalled" : row.status || "reindexing";
207
225
  return {
208
- status,
209
- startedAt,
210
- finishedAt,
211
- heartbeatAt,
212
- processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,
213
- totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,
214
- partitions,
215
- scope: scopeCandidate ? {
216
- status: (() => {
217
- const heartbeatDate = scopeCandidate.row.heartbeat_at ? new Date(scopeCandidate.row.heartbeat_at) : null;
218
- const finishedDate = scopeCandidate.row.finished_at ? new Date(scopeCandidate.row.finished_at) : null;
219
- if (finishedDate) return scopeCandidate.row.status === "failed" ? "failed" : "completed";
220
- if (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS) {
221
- return "stalled";
222
- }
223
- return scopeCandidate.row.status || "reindexing";
224
- })(),
225
- processedCount: scopeCandidate.row.processed_count ?? null,
226
- totalCount: scopeCandidate.row.total_count ?? null
227
- } : null
226
+ partitionIndex: row.partition_index ?? null,
227
+ partitionCount: row.partition_count ?? null,
228
+ status: state,
229
+ startedAt: startedDate ? startedDate.toISOString() : null,
230
+ finishedAt: finishedDate ? finishedDate.toISOString() : null,
231
+ heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,
232
+ processedCount: row.processed_count ?? null,
233
+ totalCount: row.total_count ?? null
228
234
  };
229
- } catch {
230
- return { status: "idle", partitions: [] };
235
+ }).sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0));
236
+ const activePartitions = partitions.filter((p) => !p.finishedAt);
237
+ const runningPartitions = activePartitions.filter(
238
+ (p) => p.status === "reindexing" || p.status === "purging"
239
+ );
240
+ const stalledPartitions = activePartitions.filter((p) => p.status === "stalled");
241
+ const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null;
242
+ let status = "idle";
243
+ if (activePartitions.length) {
244
+ if (runningPartitions.length) {
245
+ status = runningPartitions.some((p) => p.status === "purging") ? "purging" : "reindexing";
246
+ } else if (stalledPartitions.length) {
247
+ status = "stalled";
248
+ }
249
+ } else if (partitions.some((p) => p.status === "failed") || scopeCandidate?.row.finished_at && scopeCandidate.row.status === "failed") {
250
+ status = "failed";
231
251
  }
252
+ const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null;
253
+ const finishedAt = status === "idle" || status === "failed" ? partitions.find((p) => p.finishedAt)?.finishedAt ?? null : null;
254
+ const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null;
255
+ const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0);
256
+ const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0);
257
+ const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null;
258
+ return {
259
+ status,
260
+ startedAt,
261
+ finishedAt,
262
+ heartbeatAt,
263
+ processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,
264
+ totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,
265
+ partitions,
266
+ scope: scopeCandidate ? {
267
+ status: (() => {
268
+ const heartbeatDate = scopeCandidate.row.heartbeat_at ? new Date(scopeCandidate.row.heartbeat_at) : null;
269
+ const finishedDate = scopeCandidate.row.finished_at ? new Date(scopeCandidate.row.finished_at) : null;
270
+ if (finishedDate) return scopeCandidate.row.status === "failed" ? "failed" : "completed";
271
+ if (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS) {
272
+ return "stalled";
273
+ }
274
+ return scopeCandidate.row.status || "reindexing";
275
+ })(),
276
+ processedCount: scopeCandidate.row.processed_count ?? null,
277
+ totalCount: scopeCandidate.row.total_count ?? null
278
+ } : null
279
+ };
232
280
  }
233
281
  const normalizeCount = (value) => {
234
282
  if (value == null) return null;
@@ -242,7 +290,15 @@ async function GET(req) {
242
290
  withDeleted: false
243
291
  };
244
292
  const entitiesNeedingRefresh = /* @__PURE__ */ new Set();
245
- const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope });
293
+ const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: generatedIds, ...coverageScope });
294
+ const hasIndexCoverage = (entityId) => {
295
+ const snapshot = snapshotByEntity.get(entityId);
296
+ if (!snapshot) return false;
297
+ return snapshot.baseCount > 0 || snapshot.indexedCount > 0 || snapshot.vectorIndexedCount > 0;
298
+ };
299
+ const entityIds = generatedIds.filter(
300
+ (id) => searchConfiguredEntities.has(id) || customFieldEntities.has(id) || hasIndexCoverage(id)
301
+ );
246
302
  if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {
247
303
  await mapWithConcurrency(
248
304
  entityIds,
@@ -253,7 +309,7 @@ async function GET(req) {
253
309
  for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot);
254
310
  }
255
311
  const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null);
256
- const jobs = await Promise.all(entityIds.map((eid) => fetchJobSummary(eid, tenantId, organizationId)));
312
+ const jobsByEntity = await fetchJobSummaries(entityIds, tenantId, organizationId);
257
313
  const items = [];
258
314
  for (let idx = 0; idx < entityIds.length; idx += 1) {
259
315
  const eid = entityIds[idx];
@@ -261,11 +317,11 @@ async function GET(req) {
261
317
  const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null;
262
318
  const isStale = !coverage || !refreshedAt || Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS;
263
319
  if (isStale) entitiesNeedingRefresh.add(eid);
264
- const job = jobs[idx];
320
+ const job = jobsByEntity.get(eid) ?? idleJobSummary();
265
321
  const label = byId.get(eid)?.label || eid;
266
322
  const baseCountNumber = normalizeCount(coverage?.baseCount);
267
323
  const indexCountNumber = normalizeCount(coverage?.indexedCount);
268
- const vectorEnabled = vectorEnabledEntities.has(eid);
324
+ const vectorEnabled = vectorConfiguredEntities.has(eid);
269
325
  const vectorCountNumber = vectorEnabled ? normalizeCount(coverage?.vectorIndexedCount ?? coverage?.vector_indexed_count) : null;
270
326
  const fulltextEnabled = fulltextEnabledEntities.has(eid);
271
327
  const fulltextCountNumber = fulltextEnabled ? fulltextEntityCounts?.[eid] ?? 0 : null;
@@ -275,16 +331,20 @@ async function GET(req) {
275
331
  if (!vectorEnabled) return true;
276
332
  return vectorCountNumber != null && vectorCountNumber === baseCountNumber;
277
333
  })();
334
+ const queryIndexOk = baseCountNumber != null && indexCountNumber != null && baseCountNumber === indexCountNumber;
278
335
  items.push({
279
336
  entityId: eid,
280
337
  label,
281
338
  baseCount: baseCountNumber,
282
339
  indexCount: indexCountNumber,
283
- vectorCount: vectorEnabled ? vectorCountNumber : null,
340
+ vectorCount: vectorCountNumber,
284
341
  vectorEnabled,
342
+ vectorIndexingActive: vectorEnabled && vectorRuntimeEnabled,
285
343
  fulltextCount: fulltextCountNumber,
286
344
  fulltextEnabled,
345
+ hasCustomFields: customFieldEntities.has(eid),
287
346
  ok,
347
+ queryIndexOk,
288
348
  job,
289
349
  refreshedAt: refreshedAt ?? null
290
350
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/api/status.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { readCoverageSnapshots, refreshCoverageSnapshot, type CoverageSnapshot } from '../lib/coverage'\nimport { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from './openapi'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['query_index.status.view'] },\n}\n\nconst STATUS_REFRESH_COOLDOWN_MS = 60_000\n\nfunction getCoverageSnapshotRefreshedAt(snapshot: Pick<CoverageSnapshot, 'refreshed_at'> | null | undefined): number | null {\n const value = snapshot?.refreshed_at\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isFinite(time) ? time : null\n }\n if (typeof value === 'string') {\n const time = new Date(value).getTime()\n return Number.isFinite(time) ? time : null\n }\n return null\n}\n\nfunction hasFreshCoverageSnapshots(\n snapshots: Map<string, CoverageSnapshot>,\n entityIds: string[],\n now: number,\n): boolean {\n for (const entityId of entityIds) {\n const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId))\n if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false\n }\n return entityIds.length > 0\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const db = (em as any).getKysely()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = typeof scope.tenantId === 'string' && scope.tenantId.trim().length > 0\n ? scope.tenantId.trim()\n : (typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null)\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const organizationFilter =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : organizationId\n ? [organizationId]\n : []\n\n if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const organizationScopeIds = organizationFilter === null\n ? null\n : Array.from(\n new Set(\n organizationFilter.filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n ),\n ),\n )\n\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const forceRefresh = url.searchParams.has('refresh') && url.searchParams.get('refresh') !== '0'\n\n const generatedIds = flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }))\n\n const byId = new Map<string, { entityId: string; label: string }>()\n for (const g of generated) byId.set(g.entityId, g)\n\n let entityIds = generatedIds.slice()\n\n // Resolve search module configs to determine vector-enabled entities\n // Entities with buildSource defined are vector-search enabled\n let searchModuleConfigs: SearchModuleConfig[] = []\n try {\n searchModuleConfigs = container.resolve('searchModuleConfigs') as SearchModuleConfig[]\n } catch {\n // Search module configs not available\n }\n\n const vectorEnabledEntities = new Set<string>()\n const fulltextEnabledEntities = new Set<string>()\n for (const moduleConfig of searchModuleConfigs) {\n for (const entity of moduleConfig.entities ?? []) {\n if (entity.enabled !== false) {\n // Vector: entities with buildSource defined\n if (typeof entity.buildSource === 'function') {\n vectorEnabledEntities.add(entity.entityId)\n }\n // Fulltext: entities with fieldPolicy defined\n if (entity.fieldPolicy && typeof entity.fieldPolicy === 'object') {\n fulltextEnabledEntities.add(entity.entityId)\n }\n }\n }\n }\n\n // Resolve fulltext strategy for entity counts\n let fulltextStrategy: FullTextSearchStrategy | null = null\n try {\n const searchStrategies = container.resolve('searchStrategies') as unknown[]\n fulltextStrategy = (searchStrategies?.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy) ?? null\n } catch {\n fulltextStrategy = null\n }\n\n // Fetch fulltext entity counts\n let fulltextEntityCounts: Record<string, number> | null = null\n if (fulltextStrategy) {\n try {\n fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId)\n } catch {\n fulltextEntityCounts = null\n }\n }\n\n // Limit to entities that have active custom field definitions in current scope\n try {\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .where('is_active' as any, '=', true)\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds)) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, 'in', organizationScopeIds),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n const cfRows = await cfQuery.execute() as Array<{ entity_id: string }>\n const enabled = new Set<string>((cfRows || []).map((r) => String(r.entity_id)))\n entityIds = entityIds.filter((id) => enabled.has(id))\n } catch {}\n\n const HEARTBEAT_STALE_MS = 60_000\n const COVERAGE_STALE_MS = 60_000\n const COVERAGE_REFRESH_CONCURRENCY = 8\n\n async function fetchJobSummary(entityType: string, tenantIdParam: string | null, organizationIdParam: string | null) {\n try {\n let jobQuery = db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, '=', entityType)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantIdParam ?? null}`)\n if (organizationIdParam != null) {\n jobQuery = jobQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationIdParam),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n jobQuery = jobQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)\n }\n const rows = await jobQuery\n .orderBy('started_at' as any, 'desc')\n .execute() as Array<Record<string, any>>\n\n if (!rows.length) {\n return { status: 'idle' as const, partitions: [] as any[] }\n }\n\n const preferOrg =\n organizationIdParam != null && rows.some((row: any) => row.organization_id === organizationIdParam)\n const pickPreferred = <T extends { startedTs: number; tenantMatch: boolean; orgMatch: boolean }>(\n existing: T | null,\n candidate: T,\n ): T => {\n if (!existing) return candidate\n if (preferOrg) {\n if (candidate.orgMatch && !existing.orgMatch) return candidate\n if (!candidate.orgMatch && existing.orgMatch) return existing\n }\n if (candidate.tenantMatch && !existing.tenantMatch) return candidate\n if (!candidate.tenantMatch && existing.tenantMatch) return existing\n return candidate.startedTs > existing.startedTs ? candidate : existing\n }\n\n const partitionRows = new Map<string, { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean }>()\n let scopeRow: { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean } | null = null\n for (const row of rows) {\n const key = String(row.partition_index ?? '__null__')\n const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0\n const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true\n const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null\n const candidate = { row, startedTs, tenantMatch, orgMatch }\n if (row.partition_index == null) {\n scopeRow = pickPreferred(scopeRow, candidate)\n continue\n }\n const existing = partitionRows.get(key)\n partitionRows.set(key, pickPreferred(existing ?? null, candidate))\n }\n\n const partitions = Array.from(partitionRows.values())\n .filter((entry) => !preferOrg || entry.orgMatch)\n .map(({ row }) => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const startedDate = row.started_at ? new Date(row.started_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n const stalled =\n !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)\n const state = finishedDate\n ? (row.status === 'failed' ? 'failed' : 'completed')\n : stalled\n ? 'stalled'\n : (row.status as string) || 'reindexing'\n return {\n partitionIndex: row.partition_index ?? null,\n partitionCount: row.partition_count ?? null,\n status: state,\n startedAt: startedDate ? startedDate.toISOString() : null,\n finishedAt: finishedDate ? finishedDate.toISOString() : null,\n heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,\n processedCount: row.processed_count ?? null,\n totalCount: row.total_count ?? null,\n }\n })\n .sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0))\n const activePartitions = partitions.filter((p) => !p.finishedAt)\n const runningPartitions = activePartitions.filter(\n (p) => p.status === 'reindexing' || p.status === 'purging',\n )\n const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')\n const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null\n let status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed' = 'idle'\n if (activePartitions.length) {\n if (runningPartitions.length) {\n status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'\n } else if (stalledPartitions.length) {\n status = 'stalled'\n }\n } else if (\n partitions.some((p) => p.status === 'failed')\n || (scopeCandidate?.row.finished_at && scopeCandidate.row.status === 'failed')\n ) {\n // The run finished but lost records; without this it reports \"idle\" and the only\n // hint that anything went wrong is the coverage percentage.\n status = 'failed'\n }\n\n const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null\n const finishedAt = status === 'idle' || status === 'failed'\n ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null)\n : null\n const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null\n const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)\n const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)\n const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null\n\n return {\n status,\n startedAt,\n finishedAt,\n heartbeatAt,\n processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,\n totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,\n partitions,\n scope: scopeCandidate\n ? {\n status: (() => {\n const heartbeatDate = scopeCandidate!.row.heartbeat_at ? new Date(scopeCandidate!.row.heartbeat_at) : null\n const finishedDate = scopeCandidate!.row.finished_at ? new Date(scopeCandidate!.row.finished_at) : null\n if (finishedDate) return scopeCandidate!.row.status === 'failed' ? 'failed' : 'completed'\n if (\n !heartbeatDate ||\n Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS\n ) {\n return 'stalled'\n }\n return (scopeCandidate!.row.status as string) || 'reindexing'\n })(),\n processedCount: scopeCandidate.row.processed_count ?? null,\n totalCount: scopeCandidate.row.total_count ?? null,\n }\n : null,\n }\n } catch {\n return { status: 'idle' as const, partitions: [] as any[] }\n }\n }\n\n const normalizeCount = (value: unknown): number | null => {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n\n const coverageScope = {\n tenantId: tenantId ?? null,\n organizationId,\n withDeleted: false,\n } as const\n const entitiesNeedingRefresh = new Set<string>()\n\n // Read every entity's coverage snapshot in a single batched query. This endpoint is\n // polled by the status table every few seconds, so the poll path must stay read-cheap:\n // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh\n // event emitted below, never inline per entity.\n const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n\n // An explicit refresh action (?refresh) may block, but only when the durable\n // coverage snapshots are stale. Recent persisted snapshots survive workers/restarts,\n // so repeated refresh requests use them instead of hammering base-table counts.\n if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {\n await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>\n refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),\n )\n const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)\n }\n\n const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)\n\n const jobs = await Promise.all(entityIds.map((eid) => fetchJobSummary(eid, tenantId, organizationId)))\n\n const items: any[] = []\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const eid = entityIds[idx]\n let coverage = coverageSnapshots[idx]\n\n const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null\n const isStale = !coverage || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)\n if (isStale) entitiesNeedingRefresh.add(eid)\n\n const job = jobs[idx]\n const label = (byId.get(eid)?.label) || eid\n const baseCountNumber = normalizeCount(coverage?.baseCount)\n const indexCountNumber = normalizeCount(coverage?.indexedCount)\n const vectorEnabled = vectorEnabledEntities.has(eid)\n const vectorCountNumber = vectorEnabled ? normalizeCount((coverage as any)?.vectorIndexedCount ?? (coverage as any)?.vector_indexed_count) : null\n const fulltextEnabled = fulltextEnabledEntities.has(eid)\n const fulltextCountNumber = fulltextEnabled ? (fulltextEntityCounts?.[eid] ?? 0) : null\n const ok = (() => {\n if (baseCountNumber == null || indexCountNumber == null) return false\n if (baseCountNumber !== indexCountNumber) return false\n if (!vectorEnabled) return true\n return vectorCountNumber != null && vectorCountNumber === baseCountNumber\n })()\n items.push({\n entityId: eid,\n label,\n baseCount: baseCountNumber,\n indexCount: indexCountNumber,\n vectorCount: vectorEnabled ? vectorCountNumber : null,\n vectorEnabled,\n fulltextCount: fulltextCountNumber,\n fulltextEnabled,\n ok,\n job,\n refreshedAt: refreshedAt ?? null,\n })\n }\n\n if (!forceRefresh) {\n try {\n const eventBus = container.resolve('eventBus')\n if (entitiesNeedingRefresh.size > 0) {\n await Promise.all(\n Array.from(entitiesNeedingRefresh).map((entityId) =>\n eventBus\n .emitEvent('query_index.coverage.refresh', {\n entityType: entityId,\n tenantId: tenantId ?? null,\n organizationId,\n delayMs: 0,\n })\n .catch(() => undefined)\n )\n )\n }\n } catch {}\n }\n\n let errorQuery = db\n .selectFrom('indexer_error_logs' as any)\n .selectAll()\n if (tenantId != null) {\n errorQuery = errorQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n errorQuery = errorQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n errorQuery = errorQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n errorQuery = errorQuery.where('organization_id' as any, 'is', null as any)\n }\n const errorRows = await errorQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const errors = errorRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n stack: row.stack ?? null,\n payload: row.payload ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n let logsQuery = db\n .selectFrom('indexer_status_logs' as any)\n .selectAll()\n if (tenantId != null) {\n logsQuery = logsQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n logsQuery = logsQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n logsQuery = logsQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n logsQuery = logsQuery.where('organization_id' as any, 'is', null as any)\n }\n const logRows = await logsQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const logs = logRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n const level = row.level === 'warn' ? 'warn' : 'info'\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n level,\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n details: row.details ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n const response = NextResponse.json({ items, errors, logs })\n const partial = items.find((item) => {\n // Coverage not computed yet (no snapshot) \u2014 pending an async refresh, not a partial\n // index. Do not raise the partial-index warning while counts are still unknown.\n if (item.baseCount == null && item.indexCount == null) return false\n if (item.baseCount == null || item.indexCount == null) return true\n return item.baseCount !== item.indexCount\n })\n if (partial) {\n response.headers.set(\n 'x-om-partial-index',\n JSON.stringify({\n type: 'partial_index',\n entity: partial.entityId,\n entityLabel: partial.label ?? partial.entityId,\n baseCount: partial.baseCount,\n indexedCount: partial.indexCount,\n scope: organizationId,\n })\n )\n }\n return response\n}\n\nconst queryIndexStatusDoc: OpenApiMethodDoc = {\n summary: 'Inspect query index coverage',\n description: 'Returns entity counts comparing base tables with the query index along with the latest job status.',\n tags: [queryIndexTag],\n responses: [\n { status: 200, description: 'Current query index status.', schema: queryIndexStatusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Tenant or organization context required', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Query index status',\n methods: {\n GET: queryIndexStatusDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { readCoverageSnapshots, refreshCoverageSnapshot, type CoverageSnapshot } from '../lib/coverage'\nimport { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'\nimport type { FullTextSearchStrategy } from '@open-mercato/search/strategies'\nimport type { SearchModuleConfig } from '@open-mercato/shared/modules/search'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexStatusResponseSchema } from './openapi'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport {\n envDisablesAutoIndexing,\n SEARCH_AUTO_INDEX_CONFIG_KEY,\n SEARCH_AUTO_INDEX_CONFIG_MODULE,\n} from '@open-mercato/shared/lib/search/auto-indexing'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['query_index.status.view'] },\n}\n\nconst STATUS_REFRESH_COOLDOWN_MS = 60_000\n\nfunction getCoverageSnapshotRefreshedAt(snapshot: Pick<CoverageSnapshot, 'refreshed_at'> | null | undefined): number | null {\n const value = snapshot?.refreshed_at\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isFinite(time) ? time : null\n }\n if (typeof value === 'string') {\n const time = new Date(value).getTime()\n return Number.isFinite(time) ? time : null\n }\n return null\n}\n\nfunction hasFreshCoverageSnapshots(\n snapshots: Map<string, CoverageSnapshot>,\n entityIds: string[],\n now: number,\n): boolean {\n for (const entityId of entityIds) {\n const refreshedAt = getCoverageSnapshotRefreshedAt(snapshots.get(entityId))\n if (refreshedAt === null || now - refreshedAt >= STATUS_REFRESH_COOLDOWN_MS) return false\n }\n return entityIds.length > 0\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const db = (em as any).getKysely()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationId = scope.selectedId ?? auth.orgId ?? null\n const tenantId = typeof scope.tenantId === 'string' && scope.tenantId.trim().length > 0\n ? scope.tenantId.trim()\n : (typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null)\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const organizationFilter =\n scope.filterIds === null\n ? null\n : Array.isArray(scope.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : organizationId\n ? [organizationId]\n : []\n\n if (Array.isArray(organizationFilter) && organizationFilter.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const organizationScopeIds = organizationFilter === null\n ? null\n : Array.from(\n new Set(\n organizationFilter.filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n ),\n ),\n )\n\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length === 0) {\n return NextResponse.json({ error: 'Organization access denied' }, { status: 403 })\n }\n\n const url = new URL(req.url)\n const forceRefresh = url.searchParams.has('refresh') && url.searchParams.get('refresh') !== '0'\n\n const generatedIds = flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n const generated = generatedIds.map((entityId) => ({ entityId, label: entityId }))\n\n const byId = new Map<string, { entityId: string; label: string }>()\n for (const g of generated) byId.set(g.entityId, g)\n\n // Resolve search module configs to determine which entities each search backend covers.\n // Entities with buildSource defined are vector-search capable; entities with a fieldPolicy\n // are fulltext-capable.\n let searchModuleConfigs: SearchModuleConfig[] = []\n try {\n searchModuleConfigs = container.resolve('searchModuleConfigs') as SearchModuleConfig[]\n } catch {\n // Search module configs not available\n }\n\n const vectorConfiguredEntities = new Set<string>()\n const fulltextEnabledEntities = new Set<string>()\n const searchConfiguredEntities = new Set<string>()\n for (const moduleConfig of searchModuleConfigs) {\n for (const entity of moduleConfig.entities ?? []) {\n if (entity.enabled !== false) {\n // Vector: entities with buildSource defined\n if (typeof entity.buildSource === 'function') {\n vectorConfiguredEntities.add(entity.entityId)\n searchConfiguredEntities.add(entity.entityId)\n }\n // Fulltext: entities with fieldPolicy defined\n if (entity.fieldPolicy && typeof entity.fieldPolicy === 'object') {\n fulltextEnabledEntities.add(entity.entityId)\n searchConfiguredEntities.add(entity.entityId)\n }\n }\n }\n }\n\n let searchStrategies: unknown[] = []\n try {\n searchStrategies = (container.resolve('searchStrategies') as unknown[]) ?? []\n } catch {\n searchStrategies = []\n }\n\n // Resolve fulltext strategy for entity counts\n const fulltextStrategy = (searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy) ?? null\n\n // Vector coverage is only a meaningful signal when embeddings can actually be written:\n // the instance has not switched auto-indexing off, an embedding provider is reachable,\n // and the tenant has not opted out. Reporting a permanent `0 / n` gap on installs with\n // no embedding provider trains operators to ignore this page.\n const vectorRuntimeEnabled = await (async () => {\n if (vectorConfiguredEntities.size === 0) return false\n if (envDisablesAutoIndexing()) return false\n const vectorStrategy = searchStrategies.find(\n (s: unknown) => (s as { id?: string })?.id === 'vector',\n ) as { isAvailable?: () => Promise<boolean> } | undefined\n if (typeof vectorStrategy?.isAvailable !== 'function') return false\n try {\n if (!(await vectorStrategy.isAvailable())) return false\n } catch {\n return false\n }\n try {\n const moduleConfigService = container.resolve('moduleConfigService') as ModuleConfigService\n const value = await moduleConfigService.getValue<boolean>(\n SEARCH_AUTO_INDEX_CONFIG_MODULE,\n SEARCH_AUTO_INDEX_CONFIG_KEY,\n { defaultValue: true, scope: { tenantId } },\n )\n return value !== false\n } catch {\n return true\n }\n })()\n\n // Fetch fulltext entity counts\n let fulltextEntityCounts: Record<string, number> | null = null\n if (fulltextStrategy) {\n try {\n fulltextEntityCounts = await fulltextStrategy.getEntityCounts(tenantId)\n } catch {\n fulltextEntityCounts = null\n }\n }\n\n // Entities with active custom field definitions in the current scope. This is reported\n // per row so the client can filter on it \u2014 it is NOT a gate on which entities are listed.\n const customFieldEntities = new Set<string>()\n try {\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .where('is_active' as any, '=', true)\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds)) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, 'in', organizationScopeIds),\n eb('organization_id' as any, 'is', null),\n ]))\n }\n const cfRows = await cfQuery.execute() as Array<{ entity_id: string }>\n for (const row of cfRows || []) customFieldEntities.add(String(row.entity_id))\n } catch {}\n\n const HEARTBEAT_STALE_MS = 60_000\n const COVERAGE_STALE_MS = 60_000\n const COVERAGE_REFRESH_CONCURRENCY = 8\n\n const idleJobSummary = () => ({ status: 'idle' as const, partitions: [] as any[] })\n\n // Job rows for every listed entity are fetched in a single query. This endpoint is polled\n // every few seconds and the entity list is no longer capped at custom-field entities, so a\n // per-entity round trip here would scale the poll cost with the number of indexed entities.\n async function fetchJobSummaries(\n entityTypes: string[],\n tenantIdParam: string | null,\n organizationIdParam: string | null,\n ): Promise<Map<string, ReturnType<typeof buildJobSummary>>> {\n const byEntity = new Map<string, ReturnType<typeof buildJobSummary>>()\n if (!entityTypes.length) return byEntity\n try {\n let jobQuery = db\n .selectFrom('entity_index_jobs' as any)\n .selectAll()\n .where('entity_type' as any, 'in', entityTypes)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantIdParam ?? null}`)\n if (organizationIdParam != null) {\n jobQuery = jobQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', organizationIdParam),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n jobQuery = jobQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)\n }\n const rows = await jobQuery\n .orderBy('started_at' as any, 'desc')\n .execute() as Array<Record<string, any>>\n\n const rowsByEntity = new Map<string, Array<Record<string, any>>>()\n for (const row of rows) {\n const entityType = String(row.entity_type ?? '')\n if (!entityType) continue\n const bucket = rowsByEntity.get(entityType)\n if (bucket) bucket.push(row)\n else rowsByEntity.set(entityType, [row])\n }\n for (const [entityType, entityRows] of rowsByEntity) {\n byEntity.set(entityType, buildJobSummary(entityRows, organizationIdParam, tenantIdParam))\n }\n } catch {\n return byEntity\n }\n return byEntity\n }\n\n function buildJobSummary(\n rows: Array<Record<string, any>>,\n organizationIdParam: string | null,\n tenantIdParam: string | null,\n ) {\n if (!rows.length) {\n return idleJobSummary()\n }\n\n const preferOrg =\n organizationIdParam != null && rows.some((row: any) => row.organization_id === organizationIdParam)\n const pickPreferred = <T extends { startedTs: number; tenantMatch: boolean; orgMatch: boolean }>(\n existing: T | null,\n candidate: T,\n ): T => {\n if (!existing) return candidate\n if (preferOrg) {\n if (candidate.orgMatch && !existing.orgMatch) return candidate\n if (!candidate.orgMatch && existing.orgMatch) return existing\n }\n if (candidate.tenantMatch && !existing.tenantMatch) return candidate\n if (!candidate.tenantMatch && existing.tenantMatch) return existing\n return candidate.startedTs > existing.startedTs ? candidate : existing\n }\n\n const partitionRows = new Map<string, { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean }>()\n let scopeRow: { row: any; startedTs: number; tenantMatch: boolean; orgMatch: boolean } | null = null\n for (const row of rows) {\n const key = String(row.partition_index ?? '__null__')\n const startedTs = row.started_at ? new Date(row.started_at).getTime() : 0\n const tenantMatch = tenantIdParam != null ? row.tenant_id === tenantIdParam : true\n const orgMatch = organizationIdParam != null ? row.organization_id === organizationIdParam : row.organization_id == null\n const candidate = { row, startedTs, tenantMatch, orgMatch }\n if (row.partition_index == null) {\n scopeRow = pickPreferred(scopeRow, candidate)\n continue\n }\n const existing = partitionRows.get(key)\n partitionRows.set(key, pickPreferred(existing ?? null, candidate))\n }\n\n const partitions = Array.from(partitionRows.values())\n .filter((entry) => !preferOrg || entry.orgMatch)\n .map(({ row }) => {\n const heartbeatDate = row.heartbeat_at ? new Date(row.heartbeat_at) : null\n const startedDate = row.started_at ? new Date(row.started_at) : null\n const finishedDate = row.finished_at ? new Date(row.finished_at) : null\n const stalled =\n !finishedDate && (!heartbeatDate || Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS)\n const state = finishedDate\n ? (row.status === 'failed' ? 'failed' : 'completed')\n : stalled\n ? 'stalled'\n : (row.status as string) || 'reindexing'\n return {\n partitionIndex: row.partition_index ?? null,\n partitionCount: row.partition_count ?? null,\n status: state,\n startedAt: startedDate ? startedDate.toISOString() : null,\n finishedAt: finishedDate ? finishedDate.toISOString() : null,\n heartbeatAt: heartbeatDate ? heartbeatDate.toISOString() : null,\n processedCount: row.processed_count ?? null,\n totalCount: row.total_count ?? null,\n }\n })\n .sort((a, b) => (a.partitionIndex ?? 0) - (b.partitionIndex ?? 0))\n const activePartitions = partitions.filter((p) => !p.finishedAt)\n const runningPartitions = activePartitions.filter(\n (p) => p.status === 'reindexing' || p.status === 'purging',\n )\n const stalledPartitions = activePartitions.filter((p) => p.status === 'stalled')\n const scopeCandidate = !preferOrg || !scopeRow || scopeRow.orgMatch ? scopeRow : null\n let status: 'idle' | 'reindexing' | 'purging' | 'stalled' | 'failed' = 'idle'\n if (activePartitions.length) {\n if (runningPartitions.length) {\n status = runningPartitions.some((p) => p.status === 'purging') ? 'purging' : 'reindexing'\n } else if (stalledPartitions.length) {\n status = 'stalled'\n }\n } else if (\n partitions.some((p) => p.status === 'failed')\n || (scopeCandidate?.row.finished_at && scopeCandidate.row.status === 'failed')\n ) {\n // The run finished but lost records; without this it reports \"idle\" and the only\n // hint that anything went wrong is the coverage percentage.\n status = 'failed'\n }\n\n const startedAt = activePartitions[0]?.startedAt ?? partitions[0]?.startedAt ?? null\n const finishedAt = status === 'idle' || status === 'failed'\n ? (partitions.find((p) => p.finishedAt)?.finishedAt ?? null)\n : null\n const heartbeatAt = activePartitions[0]?.heartbeatAt ?? partitions[0]?.heartbeatAt ?? null\n const jobTotalCount = partitions.reduce((sum, p) => sum + (p.totalCount ?? 0), 0)\n const processedSum = partitions.reduce((sum, p) => sum + (p.processedCount ?? 0), 0)\n const processedCount = jobTotalCount ? Math.min(jobTotalCount, processedSum) : processedSum || null\n\n return {\n status,\n startedAt,\n finishedAt,\n heartbeatAt,\n processedCount: jobTotalCount ? processedCount : scopeCandidate?.row?.processed_count ?? null,\n totalCount: jobTotalCount ? jobTotalCount : scopeCandidate?.row?.total_count ?? null,\n partitions,\n scope: scopeCandidate\n ? {\n status: (() => {\n const heartbeatDate = scopeCandidate!.row.heartbeat_at ? new Date(scopeCandidate!.row.heartbeat_at) : null\n const finishedDate = scopeCandidate!.row.finished_at ? new Date(scopeCandidate!.row.finished_at) : null\n if (finishedDate) return scopeCandidate!.row.status === 'failed' ? 'failed' : 'completed'\n if (\n !heartbeatDate ||\n Date.now() - heartbeatDate.getTime() > HEARTBEAT_STALE_MS\n ) {\n return 'stalled'\n }\n return (scopeCandidate!.row.status as string) || 'reindexing'\n })(),\n processedCount: scopeCandidate.row.processed_count ?? null,\n totalCount: scopeCandidate.row.total_count ?? null,\n }\n : null,\n }\n }\n\n const normalizeCount = (value: unknown): number | null => {\n if (value == null) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n\n const coverageScope = {\n tenantId: tenantId ?? null,\n organizationId,\n withDeleted: false,\n } as const\n const entitiesNeedingRefresh = new Set<string>()\n\n // Read every entity's coverage snapshot in a single batched query. This endpoint is\n // polled by the status table every few seconds, so the poll path must stay read-cheap:\n // stale snapshots are refreshed asynchronously via the query_index.coverage.refresh\n // event emitted below, never inline per entity.\n const snapshotByEntity = await readCoverageSnapshots(db, { entityTypes: generatedIds, ...coverageScope })\n\n const hasIndexCoverage = (entityId: string): boolean => {\n const snapshot = snapshotByEntity.get(entityId)\n if (!snapshot) return false\n return snapshot.baseCount > 0 || snapshot.indexedCount > 0 || snapshot.vectorIndexedCount > 0\n }\n\n // Nothing this page reports is custom-field dependent: `lib/indexer.ts` builds the index\n // doc from the base row and only attaches `cf:*`/`l10n:*` keys when they exist, and\n // `applyEntityIndexesJoin` runs on every query regardless of custom fields. Gating the\n // list on `custom_field_defs` hid every fully-indexed entity that happens to have no\n // custom fields, so its coverage could neither be inspected nor reindexed from here.\n // List whatever any backend actually covers and let the client filter.\n const entityIds = generatedIds.filter(\n (id) => searchConfiguredEntities.has(id) || customFieldEntities.has(id) || hasIndexCoverage(id),\n )\n\n // An explicit refresh action (?refresh) may block, but only when the durable\n // coverage snapshots are stale. Recent persisted snapshots survive workers/restarts,\n // so repeated refresh requests use them instead of hammering base-table counts.\n if (forceRefresh && entityIds.length > 0 && !hasFreshCoverageSnapshots(snapshotByEntity, entityIds, Date.now())) {\n await mapWithConcurrency(entityIds, COVERAGE_REFRESH_CONCURRENCY, (entityId) =>\n refreshCoverageSnapshot(em, { entityType: entityId, ...coverageScope }).catch(() => undefined),\n )\n const refreshed = await readCoverageSnapshots(db, { entityTypes: entityIds, ...coverageScope })\n for (const [entityId, snapshot] of refreshed) snapshotByEntity.set(entityId, snapshot)\n }\n\n const coverageSnapshots = entityIds.map((entityId) => snapshotByEntity.get(entityId) ?? null)\n\n const jobsByEntity = await fetchJobSummaries(entityIds, tenantId, organizationId)\n\n const items: any[] = []\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const eid = entityIds[idx]\n let coverage = coverageSnapshots[idx]\n\n const refreshedAt = coverage?.refreshed_at instanceof Date ? coverage.refreshed_at : coverage?.refreshed_at ? new Date(coverage.refreshed_at) : null\n const isStale = !coverage || !refreshedAt || (Date.now() - refreshedAt.getTime() > COVERAGE_STALE_MS)\n if (isStale) entitiesNeedingRefresh.add(eid)\n\n const job = jobsByEntity.get(eid) ?? idleJobSummary()\n const label = (byId.get(eid)?.label) || eid\n const baseCountNumber = normalizeCount(coverage?.baseCount)\n const indexCountNumber = normalizeCount(coverage?.indexedCount)\n // `vectorEnabled` and `vectorCount` keep their published meaning \u2014 \"the entity declares\n // buildSource\" and its raw coverage \u2014 so existing consumers see no change. Whether vector\n // indexing can actually run ships additively as `vectorIndexingActive`.\n const vectorEnabled = vectorConfiguredEntities.has(eid)\n const vectorCountNumber = vectorEnabled\n ? normalizeCount((coverage as any)?.vectorIndexedCount ?? (coverage as any)?.vector_indexed_count)\n : null\n const fulltextEnabled = fulltextEnabledEntities.has(eid)\n const fulltextCountNumber = fulltextEnabled ? (fulltextEntityCounts?.[eid] ?? 0) : null\n\n // `ok` keeps its published aggregate meaning (query index AND configured vector coverage)\n // so consumers using it as a health signal are unaffected. The narrower signal this page\n // needs \u2014 is the query index in sync with the base table \u2014 ships additively as\n // `queryIndexOk`. Folding vector into the badge is what made every vector-capable entity\n // read \"Out of sync\" while base == indexed.\n const ok = (() => {\n if (baseCountNumber == null || indexCountNumber == null) return false\n if (baseCountNumber !== indexCountNumber) return false\n if (!vectorEnabled) return true\n return vectorCountNumber != null && vectorCountNumber === baseCountNumber\n })()\n const queryIndexOk = baseCountNumber != null\n && indexCountNumber != null\n && baseCountNumber === indexCountNumber\n items.push({\n entityId: eid,\n label,\n baseCount: baseCountNumber,\n indexCount: indexCountNumber,\n vectorCount: vectorCountNumber,\n vectorEnabled,\n vectorIndexingActive: vectorEnabled && vectorRuntimeEnabled,\n fulltextCount: fulltextCountNumber,\n fulltextEnabled,\n hasCustomFields: customFieldEntities.has(eid),\n ok,\n queryIndexOk,\n job,\n refreshedAt: refreshedAt ?? null,\n })\n }\n\n if (!forceRefresh) {\n try {\n const eventBus = container.resolve('eventBus')\n if (entitiesNeedingRefresh.size > 0) {\n await Promise.all(\n Array.from(entitiesNeedingRefresh).map((entityId) =>\n eventBus\n .emitEvent('query_index.coverage.refresh', {\n entityType: entityId,\n tenantId: tenantId ?? null,\n organizationId,\n delayMs: 0,\n })\n .catch(() => undefined)\n )\n )\n }\n } catch {}\n }\n\n let errorQuery = db\n .selectFrom('indexer_error_logs' as any)\n .selectAll()\n if (tenantId != null) {\n errorQuery = errorQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n errorQuery = errorQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n errorQuery = errorQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n errorQuery = errorQuery.where('organization_id' as any, 'is', null as any)\n }\n const errorRows = await errorQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const errors = errorRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n stack: row.stack ?? null,\n payload: row.payload ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n let logsQuery = db\n .selectFrom('indexer_status_logs' as any)\n .selectAll()\n if (tenantId != null) {\n logsQuery = logsQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n logsQuery = logsQuery.where('tenant_id' as any, 'is', null as any)\n }\n if (Array.isArray(organizationScopeIds) && organizationScopeIds.length) {\n logsQuery = logsQuery.where('organization_id' as any, 'in', organizationScopeIds)\n } else {\n logsQuery = logsQuery.where('organization_id' as any, 'is', null as any)\n }\n const logRows = await logsQuery\n .orderBy('occurred_at' as any, 'desc')\n .limit(100)\n .execute() as Array<Record<string, any>>\n\n const logs = logRows.map((row: any) => {\n const occurredAt = row.occurred_at instanceof Date ? row.occurred_at : row.occurred_at ? new Date(row.occurred_at) : null\n const level = row.level === 'warn' ? 'warn' : 'info'\n return {\n id: String(row.id),\n source: String(row.source ?? ''),\n handler: String(row.handler ?? ''),\n level,\n entityType: row.entity_type ?? null,\n recordId: row.record_id ?? null,\n tenantId: row.tenant_id ?? null,\n organizationId: row.organization_id ?? null,\n message: String(row.message ?? ''),\n details: row.details ?? null,\n occurredAt: occurredAt ? occurredAt.toISOString() : new Date().toISOString(),\n }\n })\n\n const response = NextResponse.json({ items, errors, logs })\n const partial = items.find((item) => {\n // Coverage not computed yet (no snapshot) \u2014 pending an async refresh, not a partial\n // index. Do not raise the partial-index warning while counts are still unknown.\n if (item.baseCount == null && item.indexCount == null) return false\n if (item.baseCount == null || item.indexCount == null) return true\n return item.baseCount !== item.indexCount\n })\n if (partial) {\n response.headers.set(\n 'x-om-partial-index',\n JSON.stringify({\n type: 'partial_index',\n entity: partial.entityId,\n entityLabel: partial.label ?? partial.entityId,\n baseCount: partial.baseCount,\n indexedCount: partial.indexCount,\n scope: organizationId,\n })\n )\n }\n return response\n}\n\nconst queryIndexStatusDoc: OpenApiMethodDoc = {\n summary: 'Inspect query index coverage',\n description: 'Returns entity counts comparing base tables with the query index along with the latest job status.',\n tags: [queryIndexTag],\n responses: [\n { status: 200, description: 'Current query index status.', schema: queryIndexStatusResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Tenant or organization context required', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Query index status',\n methods: {\n GET: queryIndexStatusDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }