@open-mercato/core 0.10.1-develop.7888.1.96c8c2bd5f → 0.10.1-develop.7894.1.2c464c73e3
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/audit_logs/api/audit-logs/actions/route.js +12 -26
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +10 -4
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +3 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +36 -19
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/actions/route.ts +18 -28
- package/src/modules/audit_logs/services/actionLogService.ts +20 -18
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +2 -1
- package/src/modules/query_index/lib/coverage.ts +54 -31
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/CompanyKpiBar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EyeOff } from 'lucide-react'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n readVersionedIdSet,\n writeVersionedIdSet,\n clearVersionedPreference,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { isOpenDealStatus, isWonDealStatus } from '../../lib/dealStatus'\nimport type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'\nimport { formatCurrency } from './utils'\n\nconst STORAGE_KEY = 'om:company-detail-kpi-hidden'\nconst STORAGE_VERSION = 1\n\nfunction sumActiveDeals(deals: DealSummary[]): number {\n return deals\n .filter((d) => isOpenDealStatus(d.status))\n .reduce((sum, d) => {\n const amount = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amount) ? amount : 0)\n }, 0)\n}\n\nfunction getActiveDeals(deals: DealSummary[]): DealSummary[] {\n return deals.filter((d) => isOpenDealStatus(d.status))\n}\n\nfunction computeActivityTrend(interactions: InteractionSummary[]): KpiTrend | undefined {\n const now = Date.now()\n const weekMs = 7 * 86_400_000\n const thisWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n return d && now - new Date(d).getTime() < weekMs\n }).length\n const lastWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n if (!d) return false\n const diff = now - new Date(d).getTime()\n return diff >= weekMs && diff < weekMs * 2\n }).length\n if (lastWeek === 0 && thisWeek === 0) return undefined\n if (lastWeek === 0) return { value: 100, direction: 'up' }\n const pct = ((thisWeek - lastWeek) / lastWeek) * 100\n if (Math.abs(pct) < 0.5) return { value: 0, direction: 'unchanged' }\n return { value: Math.abs(pct), direction: pct > 0 ? 'up' : 'down' }\n}\n\nfunction computeDealTrend(deals: DealSummary[]): KpiTrend | undefined {\n const active = deals.filter((d) => isOpenDealStatus(d.status))\n if (active.length === 0) return undefined\n const now = Date.now()\n const monthMs = 30 * 86_400_000\n const recentDeals = active.filter((d) => d.createdAt && now - new Date(d.createdAt).getTime() < monthMs).length\n if (recentDeals > 0) return { value: recentDeals * 10, direction: 'up' }\n return { value: 0, direction: 'unchanged' }\n}\n\ntype CompanyKpiBarProps = {\n data: CompanyOverview\n}\n\nexport function CompanyKpiBar({ data }: CompanyKpiBarProps) {\n const t = useT()\n const locale = useLocale()\n\n const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])\n const activeDealsValue = React.useMemo(\n () => data.kpis?.activeDealsValue ?? sumActiveDeals(data.deals),\n [data.deals, data.kpis?.activeDealsValue],\n )\n const dealCurrency = data.kpis?.dealCurrency ?? activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'\n const activityTrend = React.useMemo(\n () => data.kpis?.activityTrend ?? computeActivityTrend(data.interactions),\n [data.interactions, data.kpis?.activityTrend],\n )\n const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])\n\n const ltvValue = React.useMemo(() => {\n if (data.kpis?.ltvValue !== undefined) return data.kpis.ltvValue\n const wonDeals = data.deals.filter((d) => isWonDealStatus(d.status))\n if (wonDeals.length === 0) return null\n return wonDeals.reduce((sum, d) => {\n const amt = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amt) ? amt : 0)\n }, 0)\n }, [data.deals, data.kpis?.ltvValue])\n\n const clientTenureYears = React.useMemo(() => {\n if (data.kpis?.clientTenureYears !== undefined) return data.kpis.clientTenureYears\n const allDates = data.interactions\n .map((i) => i.occurredAt ?? i.scheduledAt ?? i.createdAt)\n .filter(Boolean)\n .map((d) => new Date(d).getTime())\n if (allDates.length === 0) return null\n const earliest = Math.min(...allDates)\n return Math.floor((Date.now() - earliest) / (365.25 * 86_400_000))\n }, [data.interactions, data.kpis?.clientTenureYears])\n\n const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(\n () => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),\n )\n\n const toggleTile = React.useCallback((tileId: string) => {\n setHiddenTiles((prev) => {\n const next = new Set(prev)\n if (next.has(tileId)) next.delete(tileId)\n else next.add(tileId)\n writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)\n return next\n })\n }, [])\n\n const showAllTiles = React.useCallback(() => {\n setHiddenTiles(new Set())\n clearVersionedPreference(STORAGE_KEY)\n }, [])\n\n const kpiTiles = React.useMemo(() => [\n {\n id: 'activeDeals',\n title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'),\n value: activeDealsValue,\n trend: dealTrend,\n formatValue: (v: number) => formatCurrency(v, dealCurrency, locale),\n comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? 'pipeline' : 'pipelines'}`,\n },\n {\n id: 'activities',\n title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'),\n value: data.kpis?.activityCount ?? data.interactions.length,\n trend: activityTrend,\n comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months'),\n },\n {\n id: 'ltv',\n title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'),\n value: ltvValue,\n formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency, locale) : undefined,\n comparisonLabel: ltvValue !== null\n ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total')\n : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals'),\n },\n {\n id: 'clientSince',\n title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'),\n value: clientTenureYears,\n formatValue: clientTenureYears !== null\n ? (v: number) => v < 1\n ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}`\n : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}`\n : undefined,\n comparisonLabel: clientTenureYears !== null\n ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => isWonDealStatus(d.status)).length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}`\n : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet'),\n },\n ], [t, locale, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])\n\n const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id))\n\n return (\n <div className=\"space-y-2\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {visibleTiles.map((tile) => (\n <div key={tile.id} className=\"group relative\">\n <KpiCard\n title={tile.title}\n value={tile.value}\n trend={tile.trend}\n formatValue={tile.formatValue}\n comparisonLabel={tile.comparisonLabel}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={() => toggleTile(tile.id)}\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-60\"\n aria-label={t('customers.companies.dashboard.hideTile', 'Hide tile')}\n >\n <EyeOff className=\"size-3.5\" />\n </IconButton>\n </div>\n ))}\n </div>\n {hiddenTiles.size > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {t('customers.companies.dashboard.hiddenTiles', '{{count}} tiles hidden', { count: hiddenTiles.size })}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-1.5 py-0.5 text-xs hover:bg-transparent\"\n onClick={showAllTiles}\n >\n {t('customers.companies.dashboard.showAll', 'Show all')}\n </Button>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { EyeOff } from 'lucide-react'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport {\n readVersionedIdSet,\n writeVersionedIdSet,\n clearVersionedPreference,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { isOpenDealStatus, isWonDealStatus } from '../../lib/dealStatus'\nimport type { CompanyOverview, DealSummary, InteractionSummary } from '../formConfig'\nimport { formatCurrency } from './utils'\n\nconst STORAGE_KEY = 'om:company-detail-kpi-hidden'\nconst STORAGE_VERSION = 1\n\nfunction sumActiveDeals(deals: DealSummary[]): number {\n return deals\n .filter((d) => isOpenDealStatus(d.status))\n .reduce((sum, d) => {\n const amount = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amount) ? amount : 0)\n }, 0)\n}\n\nfunction getActiveDeals(deals: DealSummary[]): DealSummary[] {\n return deals.filter((d) => isOpenDealStatus(d.status))\n}\n\nfunction computeActivityTrend(interactions: InteractionSummary[]): KpiTrend | undefined {\n const now = Date.now()\n const weekMs = 7 * 86_400_000\n const thisWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n return d && now - new Date(d).getTime() < weekMs\n }).length\n const lastWeek = interactions.filter((i) => {\n const d = i.occurredAt ?? i.scheduledAt\n if (!d) return false\n const diff = now - new Date(d).getTime()\n return diff >= weekMs && diff < weekMs * 2\n }).length\n if (lastWeek === 0 && thisWeek === 0) return undefined\n if (lastWeek === 0) return { value: 100, direction: 'up' }\n const pct = ((thisWeek - lastWeek) / lastWeek) * 100\n if (Math.abs(pct) < 0.5) return { value: 0, direction: 'unchanged' }\n return { value: Math.abs(pct), direction: pct > 0 ? 'up' : 'down' }\n}\n\nfunction computeDealTrend(deals: DealSummary[]): KpiTrend | undefined {\n const active = deals.filter((d) => isOpenDealStatus(d.status))\n if (active.length === 0) return undefined\n const now = Date.now()\n const monthMs = 30 * 86_400_000\n const recentDeals = active.filter((d) => d.createdAt && now - new Date(d.createdAt).getTime() < monthMs).length\n if (recentDeals > 0) return { value: recentDeals * 10, direction: 'up' }\n return { value: 0, direction: 'unchanged' }\n}\n\ntype CompanyKpiBarProps = {\n data: CompanyOverview\n}\n\nexport function CompanyKpiBar({ data }: CompanyKpiBarProps) {\n const t = useT()\n const locale = useLocale()\n\n const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])\n const activeDealsValue = React.useMemo(\n () => data.kpis?.activeDealsValue ?? sumActiveDeals(data.deals),\n [data.deals, data.kpis?.activeDealsValue],\n )\n const dealCurrency = data.kpis?.dealCurrency ?? activeDeals[0]?.valueCurrency ?? data.deals[0]?.valueCurrency ?? 'PLN'\n const activityTrend = React.useMemo(\n () => data.kpis?.activityTrend ?? computeActivityTrend(data.interactions),\n [data.interactions, data.kpis?.activityTrend],\n )\n const dealTrend = React.useMemo(() => computeDealTrend(data.deals), [data.deals])\n\n const ltvValue = React.useMemo(() => {\n if (data.kpis?.ltvValue !== undefined) return data.kpis.ltvValue\n const wonDeals = data.deals.filter((d) => isWonDealStatus(d.status))\n if (wonDeals.length === 0) return null\n return wonDeals.reduce((sum, d) => {\n const amt = typeof d.valueAmount === 'number' ? d.valueAmount : parseFloat(String(d.valueAmount ?? '0'))\n return sum + (Number.isFinite(amt) ? amt : 0)\n }, 0)\n }, [data.deals, data.kpis?.ltvValue])\n\n const clientTenureYears = React.useMemo(() => {\n if (data.kpis?.clientTenureYears !== undefined) return data.kpis.clientTenureYears\n const allDates = data.interactions\n .map((i) => i.occurredAt ?? i.scheduledAt ?? i.createdAt)\n .filter(Boolean)\n .map((d) => new Date(d).getTime())\n if (allDates.length === 0) return null\n const earliest = Math.min(...allDates)\n return Math.floor((Date.now() - earliest) / (365.25 * 86_400_000))\n }, [data.interactions, data.kpis?.clientTenureYears])\n\n const [hiddenTiles, setHiddenTiles] = React.useState<Set<string>>(\n () => readVersionedIdSet(STORAGE_KEY, STORAGE_VERSION),\n )\n\n const toggleTile = React.useCallback((tileId: string) => {\n setHiddenTiles((prev) => {\n const next = new Set(prev)\n if (next.has(tileId)) next.delete(tileId)\n else next.add(tileId)\n writeVersionedIdSet(STORAGE_KEY, STORAGE_VERSION, next)\n return next\n })\n }, [])\n\n const showAllTiles = React.useCallback(() => {\n setHiddenTiles(new Set())\n clearVersionedPreference(STORAGE_KEY)\n }, [])\n\n const kpiTiles = React.useMemo(() => [\n {\n id: 'activeDeals',\n title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'),\n value: activeDealsValue,\n trend: dealTrend,\n formatValue: (v: number) => formatCurrency(v, dealCurrency, locale),\n comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? 'pipeline' : 'pipelines'}`,\n },\n {\n id: 'activities',\n title: t('customers.companies.dashboard.kpi.activities', 'ACTIVITIES'),\n value: data.kpis?.activityCount ?? data.interactions.length,\n trend: activityTrend,\n comparisonLabel: t('customers.companies.dashboard.kpi.last12months', 'last 12 months'),\n },\n {\n id: 'ltv',\n title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'),\n value: ltvValue,\n formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency, locale) : undefined,\n comparisonLabel: ltvValue !== null\n ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total')\n : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals'),\n },\n {\n id: 'clientSince',\n title: t('customers.companies.dashboard.kpi.clientSince', 'CLIENT SINCE'),\n value: clientTenureYears,\n formatValue: clientTenureYears !== null\n ? (v: number) => v < 1\n ? `< 1 ${t('customers.companies.dashboard.kpi.year', 'year')}`\n : `${v} ${v === 1 ? t('customers.companies.dashboard.kpi.year', 'year') : t('customers.companies.dashboard.kpi.years', 'years')}`\n : undefined,\n comparisonLabel: clientTenureYears !== null\n ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => isWonDealStatus(d.status)).length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}`\n : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet'),\n },\n ], [t, locale, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])\n\n const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id))\n\n return (\n <div className=\"space-y-2\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {visibleTiles.map((tile) => (\n <div key={tile.id} data-testid=\"company-kpi-tile\" className=\"group relative flex\">\n <KpiCard\n title={tile.title}\n value={tile.value}\n trend={tile.trend}\n formatValue={tile.formatValue}\n comparisonLabel={tile.comparisonLabel}\n className=\"w-full\"\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n onClick={() => toggleTile(tile.id)}\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-60\"\n aria-label={t('customers.companies.dashboard.hideTile', 'Hide tile')}\n >\n <EyeOff className=\"size-3.5\" />\n </IconButton>\n </div>\n ))}\n </div>\n {hiddenTiles.size > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {t('customers.companies.dashboard.hiddenTiles', '{{count}} tiles hidden', { count: hiddenTiles.size })}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-1.5 py-0.5 text-xs hover:bg-transparent\"\n onClick={showAllTiles}\n >\n {t('customers.companies.dashboard.showAll', 'Show all')}\n </Button>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -85,6 +85,7 @@ async function updateCoverageRows(db, scope, deltas) {
|
|
|
85
85
|
return update.returning(["id"]).execute();
|
|
86
86
|
}
|
|
87
87
|
async function incrementCoverageRow(db, scope, deltas) {
|
|
88
|
+
if (scope.tenantId == null) await lockCoverageInitialization(db, scope);
|
|
88
89
|
const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null);
|
|
89
90
|
let deltaBase = deltas.deltaBase;
|
|
90
91
|
let deltaIndex = deltas.deltaIndex;
|
|
@@ -99,11 +100,7 @@ async function incrementCoverageRow(db, scope, deltas) {
|
|
|
99
100
|
deltaVector += toCount(row.vector_indexed_count);
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
-
if (updated.length === 0 && scope.tenantId == null) {
|
|
104
|
-
await lockCoverageInitialization(db, scope);
|
|
105
|
-
updated = await updateCoverageRows(db, scope, { deltaBase, deltaIndex, deltaVector });
|
|
106
|
-
}
|
|
103
|
+
const updated = await updateCoverageRows(db, scope, { deltaBase, deltaIndex, deltaVector });
|
|
107
104
|
if (updated.length > 0) {
|
|
108
105
|
if (updated.length > 1) await pruneDuplicateCoverageRows(db, scope, updated[0].id);
|
|
109
106
|
return;
|
|
@@ -133,6 +130,18 @@ async function upsertCoverageRow(db, scope, counts) {
|
|
|
133
130
|
purge = scope.tenantId == null ? purge.where("tenant_id", "is", null) : purge.where("tenant_id", "=", scope.tenantId);
|
|
134
131
|
await purge.execute();
|
|
135
132
|
}
|
|
133
|
+
if (scope.tenantId == null) {
|
|
134
|
+
const updated = await db.updateTable("entity_index_coverage").set({
|
|
135
|
+
base_count: counts.baseCount,
|
|
136
|
+
indexed_count: counts.indexedCount,
|
|
137
|
+
vector_indexed_count: counts.vectorIndexedCount,
|
|
138
|
+
refreshed_at: sql`now()`
|
|
139
|
+
}).where("entity_type", "=", scope.entityType).where("tenant_id", "is", null).where("organization_id", "=", storedOrgId).where("with_deleted", "=", scope.withDeleted === true).returning("id").execute();
|
|
140
|
+
if (updated.length > 0) {
|
|
141
|
+
if (updated.length > 1) await pruneDuplicateCoverageRows(db, scope, updated[0].id);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
136
145
|
const rows = await db.insertInto("entity_index_coverage").values({
|
|
137
146
|
entity_type: scope.entityType,
|
|
138
147
|
tenant_id: scope.tenantId ?? null,
|
|
@@ -363,20 +372,28 @@ async function writeCoverageCounts(em, scope, counts) {
|
|
|
363
372
|
const tenantId = scope.tenantId ?? null;
|
|
364
373
|
const organizationId = scope.organizationId ?? null;
|
|
365
374
|
const withDeleted = scope.withDeleted === true;
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
375
|
+
const write = async (executor) => {
|
|
376
|
+
if (tenantId == null) await lockCoverageInitialization(executor, scope);
|
|
377
|
+
const existing = await fetchCoverageRow(executor, {
|
|
378
|
+
entityType,
|
|
379
|
+
tenantId,
|
|
380
|
+
organizationId,
|
|
381
|
+
withDeleted
|
|
382
|
+
});
|
|
383
|
+
const baseCount = counts.baseCount !== void 0 ? Math.max(0, Math.trunc(toCount(counts.baseCount))) : Math.max(0, Math.trunc(toCount(existing?.base_count)));
|
|
384
|
+
const indexCount = counts.indexedCount !== void 0 ? Math.max(0, Math.trunc(toCount(counts.indexedCount))) : Math.max(0, Math.trunc(toCount(existing?.indexed_count)));
|
|
385
|
+
const vectorCount = counts.vectorCount !== void 0 ? Math.max(0, Math.trunc(toCount(counts.vectorCount))) : Math.max(0, Math.trunc(toCount(existing?.vector_indexed_count)));
|
|
386
|
+
await upsertCoverageRow(executor, { entityType, tenantId, organizationId, withDeleted }, {
|
|
387
|
+
baseCount,
|
|
388
|
+
indexedCount: indexCount,
|
|
389
|
+
vectorIndexedCount: vectorCount
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
if (tenantId == null && !db.isTransaction) {
|
|
393
|
+
await db.transaction().execute(write);
|
|
394
|
+
} else {
|
|
395
|
+
await write(db);
|
|
396
|
+
}
|
|
380
397
|
}
|
|
381
398
|
function aggregateAdjustments(adjustments) {
|
|
382
399
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/lib/coverage.ts"],
|
|
4
|
-
"sourcesContent": ["import { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, type Transaction, sql } from 'kysely'\nimport { resolveEntityTableName as resolveFallbackEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nfunction resolveEntityTableName(em: EntityManager, entity: string): string {\n return resolveQueryIndexSourceTableMetadata(em, entity)?.table ?? resolveFallbackEntityTableName(em, entity)\n}\n\nconst logger = createLogger('query_index').child({ component: 'coverage' })\n\ntype CoverageExecutor = Kysely<any> | Transaction<any>\n\nexport type CoverageScope = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\ntype CoverageRow = {\n base_count: unknown\n indexed_count: unknown\n vector_indexed_count: unknown\n refreshed_at: Date | string | null\n}\n\nexport type CoverageSnapshot = CoverageRow & {\n baseCount: number\n indexedCount: number\n vectorIndexedCount: number\n}\n\nexport type CoverageBatchScope = {\n entityTypes: readonly string[]\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\nexport type CoverageAdjustment = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n deltaBase: number\n deltaIndex: number\n deltaVector?: number\n}\n\nexport type CoverageDeltaInput = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n baseDelta: number\n indexDelta: number\n vectorDelta?: number\n}\n\nconst COLUMN_CACHE = new Map<string, boolean>()\n// In-flight de-dup: without this, N concurrent `tableHasColumn` callers for the same\n// (table, column) \u2014 e.g. every entity type's `refreshCoverageSnapshot` asking about\n// `vector_search.entity_id` \u2014 would each see a cold cache and fire their own identical\n// `information_schema.columns` query, since the cache is only populated after a query\n// resolves. Tracking the in-flight promise lets late arrivals await the first one instead.\nconst COLUMN_CACHE_PENDING = new Map<string, Promise<boolean>>()\nconst GLOBAL_ORGANIZATION_PLACEHOLDER = '00000000-0000-0000-0000-000000000000'\nexport const COVERAGE_ORG_PLACEHOLDER = GLOBAL_ORGANIZATION_PLACEHOLDER\n\nfunction toCount(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n if (value != null && typeof (value as { valueOf: () => number }).valueOf === 'function') {\n const parsed = Number((value as { valueOf: () => number }).valueOf())\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nfunction normalizeOrganizationForStore(orgId: string | null | undefined): string {\n return orgId ?? GLOBAL_ORGANIZATION_PLACEHOLDER\n}\n\nfunction applyOrganizationCondition<QB extends { where: (...args: any[]) => QB }>(\n qb: QB,\n column: string,\n organizationId: string | null | undefined,\n): QB {\n const stored = normalizeOrganizationForStore(organizationId ?? null)\n if (stored === GLOBAL_ORGANIZATION_PLACEHOLDER) {\n return qb.where((eb: any) => eb.or([\n eb(column as any, 'is', null),\n eb(column as any, '=', GLOBAL_ORGANIZATION_PLACEHOLDER),\n ]))\n }\n return qb.where(column as any, '=', stored)\n}\n\nasync function fetchCoverageRow(\n db: CoverageExecutor,\n scope: CoverageScope\n): Promise<(CoverageRow & { organization_id: string | null }) | null> {\n const { entityType, tenantId, organizationId, withDeleted } = scope\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, '=', entityType)\n .where('with_deleted' as any, '=', withDeleted === true)\n .orderBy('refreshed_at' as any, 'desc')\n query = tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', organizationId ?? null)\n const row = await query.executeTakeFirst() as (CoverageRow & { organization_id: string | null }) | undefined\n return row ?? null\n}\n\nasync function pruneDuplicateCoverageRows(\n db: CoverageExecutor,\n scope: CoverageScope,\n keepId: string | null\n): Promise<void> {\n let query = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n query = scope.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', scope.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', scope.organizationId ?? null)\n if (keepId) {\n query = query.where('id' as any, '!=', keepId)\n }\n await query.execute()\n}\n\nfunction coverageInitializationLockKey(scope: CoverageScope): string {\n return [\n scope.entityType,\n scope.tenantId ?? '__null_tenant__',\n normalizeOrganizationForStore(scope.organizationId ?? null),\n scope.withDeleted === true ? 'with_deleted' : 'active_only',\n ].join('|')\n}\n\nasync function lockCoverageInitialization(db: CoverageExecutor, scope: CoverageScope): Promise<void> {\n await sql`\n select pg_advisory_xact_lock(\n hashtextextended(${coverageInitializationLockKey(scope)}, 0)\n )\n `.execute(db)\n}\n\nasync function updateCoverageRows(\n db: CoverageExecutor,\n scope: CoverageScope,\n deltas: { deltaBase: number; deltaIndex: number; deltaVector: number },\n): Promise<Array<{ id: string }>> {\n let update = db\n .updateTable('entity_index_coverage' as any)\n .set({\n base_count: sql`greatest(${sql.ref('base_count')} + ${deltas.deltaBase}, 0)`,\n indexed_count: sql`greatest(${sql.ref('indexed_count')} + ${deltas.deltaIndex}, 0)`,\n vector_indexed_count: sql`greatest(${sql.ref('vector_indexed_count')} + ${deltas.deltaVector}, 0)`,\n refreshed_at: sql`now()`,\n } as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n update = scope.tenantId == null\n ? update.where('tenant_id' as any, 'is', null as any)\n : update.where('tenant_id' as any, '=', scope.tenantId)\n update = applyOrganizationCondition(update as any, 'organization_id', scope.organizationId ?? null)\n return update.returning(['id' as any]).execute() as Promise<Array<{ id: string }>>\n}\n\n// Applies a coverage delta by incrementing the stored columns in SQL (#5604). The obvious\n// alternative \u2014 read the row, add the delta in JavaScript, write the total back \u2014 is a\n// read-modify-write: two adjustments for the same scope that overlap both read the same row\n// and both write the same total, so one of them is silently lost. Incrementing in SQL makes\n// overlapping adjustments compose, and drops the `SELECT` (and the window between it and the\n// write) that made the coverage row a contention point in the first place.\n//\n// In steady state that is one statement. The extra statements are recovery paths that do not\n// fire once a scope has settled: the legacy NULL-organization fold below runs only for\n// global-organization scopes, the insert only until the scope's row exists, and the duplicate\n// prune only when the scope somehow holds more than one row.\n//\n// The statement is an `UPDATE` rather than an `INSERT \u2026 ON CONFLICT DO UPDATE`, because the\n// conflict target cannot see every scope this table stores: `entity_index_coverage_scope_idx`\n// is a plain `UNIQUE` constraint, and Postgres treats NULLs in one as distinct. For a scope\n// whose tenant is NULL \u2014 `resolveQueryIndexRecordScope`'s `global` branch, or any source row\n// with a NULL tenant column \u2014 the conflict branch would never fire, so every adjustment would\n// insert a fresh row carrying only its own delta and `pruneDuplicateCoverageRows` would then\n// delete the accumulated one. A NULL-aware `UPDATE` matches those rows, and it still adds the\n// delta to the column's own value, so it composes exactly the way the conflict branch does.\nasync function incrementCoverageRow(\n db: CoverageExecutor,\n scope: CoverageScope,\n deltas: { deltaBase: number; deltaIndex: number; deltaVector: number }\n): Promise<void> {\n const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null)\n let deltaBase = deltas.deltaBase\n let deltaIndex = deltas.deltaIndex\n let deltaVector = deltas.deltaVector\n\n if (scope.organizationId == null) {\n // Legacy rows stored the global scope as a NULL organization; they are replaced by the\n // placeholder-organization row. `RETURNING` carries their counts into this delta so the\n // migration does not reset the counters (the read-modify-write this replaced folded them\n // in via its `SELECT`, which matched NULL and placeholder alike).\n let purge = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n .where('organization_id' as any, 'is', null as any)\n purge = scope.tenantId == null\n ? purge.where('tenant_id' as any, 'is', null as any)\n : purge.where('tenant_id' as any, '=', scope.tenantId)\n const purged = await purge\n .returning(['base_count' as any, 'indexed_count' as any, 'vector_indexed_count' as any])\n .execute() as Array<CoverageRow>\n for (const row of purged ?? []) {\n deltaBase += toCount(row.base_count)\n deltaIndex += toCount(row.indexed_count)\n deltaVector += toCount(row.vector_indexed_count)\n }\n }\n\n let updated = await updateCoverageRows(db, scope, { deltaBase, deltaIndex, deltaVector })\n\n if (updated.length === 0 && scope.tenantId == null) {\n // The unique constraint cannot serialize a first insert whose tenant is NULL. Take a\n // transaction-scoped lock only on this rowless branch, then repeat the UPDATE: a creator\n // that won while we waited has committed its row, so this adjustment composes into it.\n await lockCoverageInitialization(db, scope)\n updated = await updateCoverageRows(db, scope, { deltaBase, deltaIndex, deltaVector })\n }\n\n if (updated.length > 0) {\n // In steady state the scope holds exactly the row just incremented, so the adjustment is\n // the single statement it is described as. Only a scope that somehow accumulated more than\n // one row \u2014 a legacy import, or a concurrent first insert on a NULL-tenant scope, which the\n // unique constraint cannot prevent \u2014 pays for the collapse, and only until it is collapsed.\n if (updated.length > 1) await pruneDuplicateCoverageRows(db, scope, updated[0]!.id)\n return\n }\n\n // No row for this scope yet. `ON CONFLICT` closes the insert race for scopes the unique\n // constraint can see. NULL-tenant initialization reaches here only while holding the scoped\n // transaction advisory lock above, so it has the same single-creator guarantee.\n const rows = await db\n .insertInto('entity_index_coverage' as any)\n .values({\n entity_type: scope.entityType,\n tenant_id: scope.tenantId ?? null,\n organization_id: storedOrgId,\n with_deleted: scope.withDeleted === true,\n base_count: Math.max(deltaBase, 0),\n indexed_count: Math.max(deltaIndex, 0),\n vector_indexed_count: Math.max(deltaVector, 0),\n refreshed_at: sql`now()`,\n } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'tenant_id', 'organization_id', 'with_deleted'])\n .doUpdateSet({\n base_count: sql`greatest(${sql.ref('entity_index_coverage.base_count')} + ${deltaBase}, 0)`,\n indexed_count: sql`greatest(${sql.ref('entity_index_coverage.indexed_count')} + ${deltaIndex}, 0)`,\n vector_indexed_count: sql`greatest(${sql.ref('entity_index_coverage.vector_indexed_count')} + ${deltaVector}, 0)`,\n refreshed_at: sql`now()`,\n } as any))\n .returning(['id' as any])\n .execute() as Array<{ id: string }>\n\n const keepId = rows?.[0]?.id ?? null\n await pruneDuplicateCoverageRows(db, scope, keepId)\n}\n\nasync function upsertCoverageRow(\n db: CoverageExecutor,\n scope: CoverageScope,\n counts: { baseCount: number; indexedCount: number; vectorIndexedCount: number }\n): Promise<void> {\n const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null)\n if (scope.organizationId == null) {\n let purge = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n .where('organization_id' as any, 'is', null as any)\n purge = scope.tenantId == null\n ? purge.where('tenant_id' as any, 'is', null as any)\n : purge.where('tenant_id' as any, '=', scope.tenantId)\n await purge.execute()\n }\n\n const rows = await db\n .insertInto('entity_index_coverage' as any)\n .values({\n entity_type: scope.entityType,\n tenant_id: scope.tenantId ?? null,\n organization_id: storedOrgId,\n with_deleted: scope.withDeleted === true,\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'tenant_id', 'organization_id', 'with_deleted'])\n .doUpdateSet({\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any))\n .returning(['id' as any])\n .execute() as Array<{ id: string }>\n\n const keepId = rows?.[0]?.id ?? null\n await pruneDuplicateCoverageRows(db, scope, keepId)\n}\n\nexport async function readCoverageSnapshot(\n db: Kysely<any>,\n scope: CoverageScope\n): Promise<(CoverageRow & { baseCount: number; indexedCount: number; vectorIndexedCount: number }) | null> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return null\n const row = await fetchCoverageRow(db, {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n withDeleted: scope.withDeleted === true,\n })\n if (!row) return null\n const refreshedAt = row.refreshed_at instanceof Date ? row.refreshed_at : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n return {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n }\n}\n\nexport async function readCoverageSnapshots(\n db: Kysely<any>,\n batch: CoverageBatchScope\n): Promise<Map<string, CoverageSnapshot>> {\n const entityTypes = Array.from(\n new Set((batch.entityTypes ?? []).map((id) => String(id || '')).filter((id) => id.length > 0))\n )\n const result = new Map<string, CoverageSnapshot>()\n if (entityTypes.length === 0) return result\n\n const withDeleted = batch.withDeleted === true\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'entity_type' as any,\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, 'in', entityTypes)\n .where('with_deleted' as any, '=', withDeleted)\n .orderBy('refreshed_at' as any, 'desc')\n query = batch.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', batch.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', batch.organizationId ?? null)\n\n const rows = await query.execute() as Array<CoverageRow & { entity_type: string }>\n for (const row of rows ?? []) {\n const entityType = String(row.entity_type || '')\n // Rows are ordered by refreshed_at desc, so the first row seen per entity is the latest.\n if (!entityType || result.has(entityType)) continue\n const refreshedAt = row.refreshed_at instanceof Date\n ? row.refreshed_at\n : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n result.set(entityType, {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n })\n }\n return result\n}\n\nexport async function applyCoverageAdjustments(\n em: EntityManager,\n adjustments: CoverageAdjustment[],\n options?: { trx?: CoverageExecutor },\n): Promise<void> {\n if (!adjustments.length) return\n const db = (em as any).getKysely() as Kysely<any>\n // `em.getKysely()` hands back the EntityManager's transaction context whenever one is open,\n // and Kysely's `Transaction` throws rather than nesting a second one. A caller already inside\n // `em.transactional(...)` therefore gets the same guarantee the rowless nullable-tenant branch\n // opens a transaction for, so use the ambient one instead of asking for another.\n const ambientTrx = (db as { isTransaction?: boolean }).isTransaction === true\n ? (db as CoverageExecutor)\n : null\n // Keep scope initialization in a stable order. A caller-supplied transaction retains\n // advisory and unique-index locks until commit, so two batches that initialize the same\n // scopes in opposite orders could otherwise deadlock.\n const aggregated = aggregateAdjustments(adjustments).sort((left, right) => (\n scopeKey(left.scope).localeCompare(scopeKey(right.scope))\n ))\n const executor = options?.trx ?? ambientTrx\n for (const entry of aggregated) {\n const deltas = {\n deltaBase: entry.deltaBase,\n deltaIndex: entry.deltaIndex,\n deltaVector: entry.deltaVector,\n }\n if (executor) {\n await incrementCoverageRow(executor, entry.scope, deltas)\n } else if (entry.scope.tenantId == null) {\n await db.transaction().execute((trx) => incrementCoverageRow(trx, entry.scope, deltas))\n } else {\n await incrementCoverageRow(db, entry.scope, deltas)\n }\n }\n}\n\nexport async function deleteCoverageForEntity(db: Kysely<any>, entityType: string): Promise<void> {\n if (!entityType) return\n await db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', entityType)\n .execute()\n}\n\ntype CatalogueCoverageDatabase = {\n entity_index_coverage: { entity_type: string; tenant_id: string | null; organization_id: string | null }\n}\n\n/**\n * Drop every coverage row of `entityType` outside the global scope (no tenant, and the\n * organization placeholder or no organization). A declared platform-wide catalogue keeps its\n * coverage there only; rows an earlier sweep wrote under the caller's scope are never\n * refreshed again and would keep reporting stale counts.\n */\nexport async function deleteScopedCoverageForEntity(db: Kysely<CatalogueCoverageDatabase>, entityType: string): Promise<void> {\n if (!entityType) return\n await db\n .deleteFrom('entity_index_coverage')\n .where('entity_type', '=', entityType)\n .where(sql<boolean>`(tenant_id is not null or (organization_id is not null and organization_id <> ${GLOBAL_ORGANIZATION_PLACEHOLDER}))`)\n .execute()\n}\n\nasync function deleteCoverageScope(db: Kysely<any>, scope: CoverageScope): Promise<void> {\n const { entityType, tenantId, organizationId, withDeleted } = scope\n if (!entityType) return\n let query = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', entityType)\n .where('with_deleted' as any, '=', withDeleted === true)\n query = tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', tenantId)\n await applyOrganizationCondition(query as any, 'organization_id', organizationId ?? null).execute()\n}\n\nasync function tableHasColumn(db: Kysely<any>, table: string, column: string): Promise<boolean> {\n const key = `${table}.${column}`\n if (COLUMN_CACHE.has(key)) return COLUMN_CACHE.get(key)!\n const pending = COLUMN_CACHE_PENDING.get(key)\n if (pending) return pending\n const promise = (async () => {\n const exists = await db\n .selectFrom('information_schema.columns' as any)\n .select(sql<number>`1`.as('present'))\n .where(table.includes('.') ? sql<boolean>`table_schema = ${table.split('.')[0]}` : sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', table.split('.').pop())\n .where('column_name' as any, '=', column)\n .executeTakeFirst()\n const present = !!exists\n COLUMN_CACHE.set(key, present)\n return present\n })()\n COLUMN_CACHE_PENDING.set(key, promise)\n try {\n return await promise\n } finally {\n COLUMN_CACHE_PENDING.delete(key)\n }\n}\n\nexport type ColumnCheck = { table: string; column: string }\n\n// Batches the `information_schema.columns` introspection used by `refreshCoverageSnapshot`\n// into a single query for a whole set of (table, column) pairs, and pre-populates\n// `COLUMN_CACHE_PENDING` for every pair before that query even runs. Callers of\n// `coverage_warmup.ts` use this so its many concurrently-dispatched `coverage.refresh`\n// subscribers hit an already-primed (or in-flight) cache instead of each doing their own\n// per-table introspection round trip.\nexport async function primeColumnCache(db: Kysely<any>, checks: ColumnCheck[]): Promise<void> {\n for (const check of checks.filter((entry) => typeof entry?.table === 'string' && entry.table.includes('.'))) await tableHasColumn(db, check.table, check.column)\n checks = checks.filter((entry) => !(typeof entry?.table === 'string' && entry.table.includes('.')))\n const missing: Array<{ table: string; column: string; key: string }> = []\n const seen = new Set<string>()\n for (const check of checks) {\n const table = String(check?.table || '')\n const column = String(check?.column || '')\n if (!table || !column) continue\n const key = `${table}.${column}`\n if (seen.has(key)) continue\n seen.add(key)\n if (COLUMN_CACHE.has(key) || COLUMN_CACHE_PENDING.has(key)) continue\n missing.push({ table, column, key })\n }\n if (!missing.length) return\n\n const tables = Array.from(new Set(missing.map((entry) => entry.table)))\n const columns = Array.from(new Set(missing.map((entry) => entry.column)))\n\n const batchPromise = (async (): Promise<Set<string>> => {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['table_name' as any, 'column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, 'in', tables)\n .where('column_name' as any, 'in', columns)\n .execute() as Array<{ table_name: string; column_name: string }>\n return new Set(rows.map((row) => `${row.table_name}.${row.column_name}`))\n })()\n\n for (const entry of missing) {\n const entryPromise = batchPromise.then((present) => {\n const value = present.has(entry.key)\n COLUMN_CACHE.set(entry.key, value)\n return value\n })\n // Mark the stored promise as handled: when the batch query fails and no\n // `tableHasColumn` caller has adopted this entry yet (the common case \u2014 the warmup\n // awaits priming before dispatching any refresh), an orphaned rejection would\n // otherwise crash a plain-Node event worker via unhandledRejection. Awaiting\n // callers still observe the rejection through the stored reference.\n entryPromise.catch(() => undefined)\n COLUMN_CACHE_PENDING.set(entry.key, entryPromise)\n }\n\n try {\n await batchPromise\n } finally {\n for (const entry of missing) COLUMN_CACHE_PENDING.delete(entry.key)\n }\n}\n\nexport async function refreshCoverageSnapshot(\n em: EntityManager,\n scope: CoverageScope,\n): Promise<{ baseCount: number; indexedCount: number } | null> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return null\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n\n const db = (em as any).getKysely() as Kysely<any>\n const baseTable = resolveEntityTableName(em, entityType)\n\n const hasOrg = await tableHasColumn(db, baseTable, 'organization_id')\n const hasTenant = await tableHasColumn(db, baseTable, 'tenant_id')\n const hasDeleted = await tableHasColumn(db, baseTable, 'deleted_at')\n\n // A scope the base table cannot express must not narrow the index side either. Index rows\n // can carry an organization the base table has no column for \u2014 `organizations` has no\n // `organization_id` yet its index rows derive one from the record id. Filtering only the\n // index side compares two different populations and reports a gap no reindex can close;\n // returning early instead of recounting left the previous snapshot frozen, which is what\n // surfaced as a permanent \"out of sync\" row for `directory:organization`.\n //\n // Tenant is different: a base table without `tenant_id` (`user_roles`) cannot be counted\n // per tenant at all, and writing the cross-tenant total into one tenant's row would leak\n // another tenant's volume into it. Drop the unusable scoped row instead, leaving only the\n // global row \u2014 which is the one that can be true \u2014 rather than freezing a stale count.\n const scopeOrg = organizationId !== null && hasOrg\n if (tenantId !== null && !hasTenant) {\n await deleteCoverageScope(db, { entityType, tenantId, organizationId, withDeleted })\n return null\n }\n const scopeTenant = tenantId !== null && hasTenant\n\n let baseQuery = db\n .selectFrom(`${baseTable} as b` as any)\n .select(sql`count(*)`.as('count'))\n if (scopeOrg) baseQuery = baseQuery.where('b.organization_id' as any, '=', organizationId)\n if (scopeTenant) baseQuery = baseQuery.where('b.tenant_id' as any, '=', tenantId)\n if (!withDeleted && hasDeleted) baseQuery = baseQuery.where('b.deleted_at' as any, 'is', null as any)\n\n let indexQuery = db\n .selectFrom('entity_indexes as ei' as any)\n .select(sql`count(*)`.as('count'))\n .where('ei.entity_type' as any, '=', entityType)\n if (scopeOrg) indexQuery = indexQuery.where('ei.organization_id' as any, '=', organizationId)\n if (scopeTenant) indexQuery = indexQuery.where('ei.tenant_id' as any, '=', tenantId)\n if (!withDeleted) indexQuery = indexQuery.where('ei.deleted_at' as any, 'is', null as any)\n\n const vectorCountPromise = (async (): Promise<number | undefined> => {\n const hasVectorTable = await tableHasColumn(db, 'vector_search', 'entity_id')\n if (!hasVectorTable || typeof tenantId !== 'string' || tenantId.length === 0) return undefined\n\n try {\n let vectorQuery = db\n .selectFrom('vector_search' as any)\n .select(sql`count(*)`.as('count'))\n .where('entity_id' as any, '=', entityType)\n .where('tenant_id' as any, '=', tenantId)\n if (organizationId !== null) {\n vectorQuery = vectorQuery.where('organization_id' as any, '=', organizationId)\n }\n const vectorRow = await vectorQuery.executeTakeFirst() as { count: unknown } | undefined\n return toCount(vectorRow?.count)\n } catch (err) {\n logger.warn('Failed to resolve vector count for coverage snapshot', {\n entityType,\n tenantId,\n organizationId,\n error: err instanceof Error ? err.message : err,\n })\n return undefined\n }\n })()\n\n const [baseRow, indexRow, vectorCount] = await Promise.all([\n baseQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n indexQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n vectorCountPromise,\n ])\n\n const baseCount = toCount(baseRow?.count)\n const indexCount = toCount(indexRow?.count)\n\n await writeCoverageCounts(em, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorCount,\n })\n\n return { baseCount, indexedCount: indexCount }\n}\n\nexport async function writeCoverageCounts(\n em: EntityManager,\n scope: CoverageScope,\n counts: { baseCount?: number; indexedCount?: number; vectorCount?: number }\n): Promise<void> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return\n const db = (em as any).getKysely() as Kysely<any>\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n const existing = await fetchCoverageRow(db, {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n })\n const baseCount = counts.baseCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.baseCount)))\n : Math.max(0, Math.trunc(toCount(existing?.base_count)))\n const indexCount = counts.indexedCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.indexedCount)))\n : Math.max(0, Math.trunc(toCount(existing?.indexed_count)))\n const vectorCount = counts.vectorCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.vectorCount)))\n : Math.max(0, Math.trunc(toCount(existing?.vector_indexed_count)))\n await upsertCoverageRow(db, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorIndexedCount: vectorCount,\n })\n}\n\ntype AggregatedAdjustment = {\n scope: CoverageScope\n deltaBase: number\n deltaIndex: number\n deltaVector: number\n}\n\nfunction aggregateAdjustments(adjustments: CoverageAdjustment[]): AggregatedAdjustment[] {\n const map = new Map<string, AggregatedAdjustment>()\n for (const adj of adjustments) {\n if (!adj?.entityType) continue\n const deltaBase = Number.isFinite(adj.deltaBase) ? adj.deltaBase : 0\n const deltaIndex = Number.isFinite(adj.deltaIndex) ? adj.deltaIndex : 0\n const deltaVector = Number.isFinite(adj.deltaVector) ? adj.deltaVector! : 0\n if (deltaBase === 0 && deltaIndex === 0 && deltaVector === 0) continue\n const scope: CoverageScope = {\n entityType: adj.entityType,\n tenantId: adj.tenantId ?? null,\n organizationId: adj.organizationId ?? null,\n withDeleted: adj.withDeleted === true,\n }\n const key = scopeKey(scope)\n const existing = map.get(key)\n if (existing) {\n existing.deltaBase += deltaBase\n existing.deltaIndex += deltaIndex\n existing.deltaVector += deltaVector\n } else {\n map.set(key, { scope, deltaBase, deltaIndex, deltaVector })\n }\n }\n return Array.from(map.values())\n}\n\nfunction scopeKey(scope: CoverageScope): string {\n const tenant = scope.tenantId ?? '__tenant_null__'\n const org = normalizeOrganizationForStore(scope.organizationId ?? null)\n const deleted = scope.withDeleted === true ? '1' : '0'\n return `${scope.entityType}|${tenant}|${org}|${deleted}`\n}\n\nexport function createCoverageAdjustments(input: CoverageDeltaInput): CoverageAdjustment[] {\n const entityType = String(input.entityType || '')\n if (!entityType) return []\n const baseDelta = Number.isFinite(input.baseDelta) ? input.baseDelta : 0\n const indexDelta = Number.isFinite(input.indexDelta) ? input.indexDelta : 0\n const vectorDelta = Number.isFinite(input.vectorDelta) ? input.vectorDelta! : 0\n if (baseDelta === 0 && indexDelta === 0 && vectorDelta === 0) return []\n const withDeleted = input.withDeleted === true\n const tenantId = input.tenantId ?? null\n const organizationId = input.organizationId ?? null\n return [\n {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n deltaBase: baseDelta,\n deltaIndex: indexDelta,\n deltaVector: vectorDelta,\n },\n ]\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { resolveQueryIndexSourceTableMetadata } from './subscriber-scope'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, type Transaction, sql } from 'kysely'\nimport { resolveEntityTableName as resolveFallbackEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nfunction resolveEntityTableName(em: EntityManager, entity: string): string {\n return resolveQueryIndexSourceTableMetadata(em, entity)?.table ?? resolveFallbackEntityTableName(em, entity)\n}\n\nconst logger = createLogger('query_index').child({ component: 'coverage' })\n\ntype CoverageExecutor = Kysely<any> | Transaction<any>\n\nexport type CoverageScope = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\ntype CoverageRow = {\n base_count: unknown\n indexed_count: unknown\n vector_indexed_count: unknown\n refreshed_at: Date | string | null\n}\n\nexport type CoverageSnapshot = CoverageRow & {\n baseCount: number\n indexedCount: number\n vectorIndexedCount: number\n}\n\nexport type CoverageBatchScope = {\n entityTypes: readonly string[]\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\nexport type CoverageAdjustment = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n deltaBase: number\n deltaIndex: number\n deltaVector?: number\n}\n\nexport type CoverageDeltaInput = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n baseDelta: number\n indexDelta: number\n vectorDelta?: number\n}\n\nconst COLUMN_CACHE = new Map<string, boolean>()\n// In-flight de-dup: without this, N concurrent `tableHasColumn` callers for the same\n// (table, column) \u2014 e.g. every entity type's `refreshCoverageSnapshot` asking about\n// `vector_search.entity_id` \u2014 would each see a cold cache and fire their own identical\n// `information_schema.columns` query, since the cache is only populated after a query\n// resolves. Tracking the in-flight promise lets late arrivals await the first one instead.\nconst COLUMN_CACHE_PENDING = new Map<string, Promise<boolean>>()\nconst GLOBAL_ORGANIZATION_PLACEHOLDER = '00000000-0000-0000-0000-000000000000'\nexport const COVERAGE_ORG_PLACEHOLDER = GLOBAL_ORGANIZATION_PLACEHOLDER\n\nfunction toCount(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n if (value != null && typeof (value as { valueOf: () => number }).valueOf === 'function') {\n const parsed = Number((value as { valueOf: () => number }).valueOf())\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nfunction normalizeOrganizationForStore(orgId: string | null | undefined): string {\n return orgId ?? GLOBAL_ORGANIZATION_PLACEHOLDER\n}\n\nfunction applyOrganizationCondition<QB extends { where: (...args: any[]) => QB }>(\n qb: QB,\n column: string,\n organizationId: string | null | undefined,\n): QB {\n const stored = normalizeOrganizationForStore(organizationId ?? null)\n if (stored === GLOBAL_ORGANIZATION_PLACEHOLDER) {\n return qb.where((eb: any) => eb.or([\n eb(column as any, 'is', null),\n eb(column as any, '=', GLOBAL_ORGANIZATION_PLACEHOLDER),\n ]))\n }\n return qb.where(column as any, '=', stored)\n}\n\nasync function fetchCoverageRow(\n db: CoverageExecutor,\n scope: CoverageScope\n): Promise<(CoverageRow & { organization_id: string | null }) | null> {\n const { entityType, tenantId, organizationId, withDeleted } = scope\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, '=', entityType)\n .where('with_deleted' as any, '=', withDeleted === true)\n .orderBy('refreshed_at' as any, 'desc')\n query = tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', organizationId ?? null)\n const row = await query.executeTakeFirst() as (CoverageRow & { organization_id: string | null }) | undefined\n return row ?? null\n}\n\nasync function pruneDuplicateCoverageRows(\n db: CoverageExecutor,\n scope: CoverageScope,\n keepId: string | null\n): Promise<void> {\n let query = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n query = scope.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', scope.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', scope.organizationId ?? null)\n if (keepId) {\n query = query.where('id' as any, '!=', keepId)\n }\n await query.execute()\n}\n\nfunction coverageInitializationLockKey(scope: CoverageScope): string {\n return [\n scope.entityType,\n scope.tenantId ?? '__null_tenant__',\n normalizeOrganizationForStore(scope.organizationId ?? null),\n scope.withDeleted === true ? 'with_deleted' : 'active_only',\n ].join('|')\n}\n\nasync function lockCoverageInitialization(db: CoverageExecutor, scope: CoverageScope): Promise<void> {\n await sql`\n select pg_advisory_xact_lock(\n hashtextextended(${coverageInitializationLockKey(scope)}, 0)\n )\n `.execute(db)\n}\n\nasync function updateCoverageRows(\n db: CoverageExecutor,\n scope: CoverageScope,\n deltas: { deltaBase: number; deltaIndex: number; deltaVector: number },\n): Promise<Array<{ id: string }>> {\n let update = db\n .updateTable('entity_index_coverage' as any)\n .set({\n base_count: sql`greatest(${sql.ref('base_count')} + ${deltas.deltaBase}, 0)`,\n indexed_count: sql`greatest(${sql.ref('indexed_count')} + ${deltas.deltaIndex}, 0)`,\n vector_indexed_count: sql`greatest(${sql.ref('vector_indexed_count')} + ${deltas.deltaVector}, 0)`,\n refreshed_at: sql`now()`,\n } as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n update = scope.tenantId == null\n ? update.where('tenant_id' as any, 'is', null as any)\n : update.where('tenant_id' as any, '=', scope.tenantId)\n update = applyOrganizationCondition(update as any, 'organization_id', scope.organizationId ?? null)\n return update.returning(['id' as any]).execute() as Promise<Array<{ id: string }>>\n}\n\n// Applies a coverage delta by incrementing the stored columns in SQL (#5604). The obvious\n// alternative \u2014 read the row, add the delta in JavaScript, write the total back \u2014 is a\n// read-modify-write: two adjustments for the same scope that overlap both read the same row\n// and both write the same total, so one of them is silently lost. Incrementing in SQL makes\n// overlapping adjustments compose, and drops the `SELECT` (and the window between it and the\n// write) that made the coverage row a contention point in the first place.\n//\n// For non-null tenants, steady state is one statement. NULL-tenant writers first take\n// the scope advisory lock because the unique constraint cannot coordinate their rows. The extra statements are recovery paths that do not\n// fire once a scope has settled: the legacy NULL-organization fold below runs only for\n// global-organization scopes, the insert only until the scope's row exists, and the duplicate\n// prune only when the scope somehow holds more than one row.\n//\n// The statement is an `UPDATE` rather than an `INSERT \u2026 ON CONFLICT DO UPDATE`, because the\n// conflict target cannot see every scope this table stores: `entity_index_coverage_scope_idx`\n// is a plain `UNIQUE` constraint, and Postgres treats NULLs in one as distinct. For a scope\n// whose tenant is NULL \u2014 `resolveQueryIndexRecordScope`'s `global` branch, or any source row\n// with a NULL tenant column \u2014 the conflict branch would never fire, so every adjustment would\n// insert a fresh row carrying only its own delta and `pruneDuplicateCoverageRows` would then\n// delete the accumulated one. A NULL-aware `UPDATE` matches those rows, and it still adds the\n// delta to the column's own value, so it composes exactly the way the conflict branch does.\nasync function incrementCoverageRow(\n db: CoverageExecutor,\n scope: CoverageScope,\n deltas: { deltaBase: number; deltaIndex: number; deltaVector: number }\n): Promise<void> {\n if (scope.tenantId == null) await lockCoverageInitialization(db, scope)\n const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null)\n let deltaBase = deltas.deltaBase\n let deltaIndex = deltas.deltaIndex\n let deltaVector = deltas.deltaVector\n\n if (scope.organizationId == null) {\n // Legacy rows stored the global scope as a NULL organization; they are replaced by the\n // placeholder-organization row. `RETURNING` carries their counts into this delta so the\n // migration does not reset the counters (the read-modify-write this replaced folded them\n // in via its `SELECT`, which matched NULL and placeholder alike).\n let purge = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n .where('organization_id' as any, 'is', null as any)\n purge = scope.tenantId == null\n ? purge.where('tenant_id' as any, 'is', null as any)\n : purge.where('tenant_id' as any, '=', scope.tenantId)\n const purged = await purge\n .returning(['base_count' as any, 'indexed_count' as any, 'vector_indexed_count' as any])\n .execute() as Array<CoverageRow>\n for (const row of purged ?? []) {\n deltaBase += toCount(row.base_count)\n deltaIndex += toCount(row.indexed_count)\n deltaVector += toCount(row.vector_indexed_count)\n }\n }\n\n const updated = await updateCoverageRows(db, scope, { deltaBase, deltaIndex, deltaVector })\n\n if (updated.length > 0) {\n // In steady state the scope holds exactly the row just incremented, so the adjustment is\n // the single statement it is described as. Only a scope that somehow accumulated more than\n // one row \u2014 a legacy import, or a concurrent first insert on a NULL-tenant scope, which the\n // unique constraint cannot prevent \u2014 pays for the collapse, and only until it is collapsed.\n if (updated.length > 1) await pruneDuplicateCoverageRows(db, scope, updated[0]!.id)\n return\n }\n\n // No row for this scope yet. `ON CONFLICT` closes the insert race for scopes the unique\n // constraint can see. NULL-tenant writes reach here only while holding the scoped\n // transaction advisory lock above, so it has the same single-creator guarantee.\n const rows = await db\n .insertInto('entity_index_coverage' as any)\n .values({\n entity_type: scope.entityType,\n tenant_id: scope.tenantId ?? null,\n organization_id: storedOrgId,\n with_deleted: scope.withDeleted === true,\n base_count: Math.max(deltaBase, 0),\n indexed_count: Math.max(deltaIndex, 0),\n vector_indexed_count: Math.max(deltaVector, 0),\n refreshed_at: sql`now()`,\n } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'tenant_id', 'organization_id', 'with_deleted'])\n .doUpdateSet({\n base_count: sql`greatest(${sql.ref('entity_index_coverage.base_count')} + ${deltaBase}, 0)`,\n indexed_count: sql`greatest(${sql.ref('entity_index_coverage.indexed_count')} + ${deltaIndex}, 0)`,\n vector_indexed_count: sql`greatest(${sql.ref('entity_index_coverage.vector_indexed_count')} + ${deltaVector}, 0)`,\n refreshed_at: sql`now()`,\n } as any))\n .returning(['id' as any])\n .execute() as Array<{ id: string }>\n\n const keepId = rows?.[0]?.id ?? null\n await pruneDuplicateCoverageRows(db, scope, keepId)\n}\n\nasync function upsertCoverageRow(\n db: CoverageExecutor,\n scope: CoverageScope,\n counts: { baseCount: number; indexedCount: number; vectorIndexedCount: number }\n): Promise<void> {\n const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null)\n if (scope.organizationId == null) {\n let purge = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n .where('organization_id' as any, 'is', null as any)\n purge = scope.tenantId == null\n ? purge.where('tenant_id' as any, 'is', null as any)\n : purge.where('tenant_id' as any, '=', scope.tenantId)\n await purge.execute()\n }\n\n if (scope.tenantId == null) {\n const updated = await db\n .updateTable('entity_index_coverage')\n .set({\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n })\n .where('entity_type', '=', scope.entityType)\n .where('tenant_id', 'is', null)\n .where('organization_id', '=', storedOrgId)\n .where('with_deleted', '=', scope.withDeleted === true)\n .returning('id')\n .execute() as Array<{ id: string }>\n if (updated.length > 0) {\n if (updated.length > 1) await pruneDuplicateCoverageRows(db, scope, updated[0]!.id)\n return\n }\n }\n\n const rows = await db\n .insertInto('entity_index_coverage' as any)\n .values({\n entity_type: scope.entityType,\n tenant_id: scope.tenantId ?? null,\n organization_id: storedOrgId,\n with_deleted: scope.withDeleted === true,\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'tenant_id', 'organization_id', 'with_deleted'])\n .doUpdateSet({\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any))\n .returning(['id' as any])\n .execute() as Array<{ id: string }>\n\n const keepId = rows?.[0]?.id ?? null\n await pruneDuplicateCoverageRows(db, scope, keepId)\n}\n\nexport async function readCoverageSnapshot(\n db: Kysely<any>,\n scope: CoverageScope\n): Promise<(CoverageRow & { baseCount: number; indexedCount: number; vectorIndexedCount: number }) | null> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return null\n const row = await fetchCoverageRow(db, {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n withDeleted: scope.withDeleted === true,\n })\n if (!row) return null\n const refreshedAt = row.refreshed_at instanceof Date ? row.refreshed_at : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n return {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n }\n}\n\nexport async function readCoverageSnapshots(\n db: Kysely<any>,\n batch: CoverageBatchScope\n): Promise<Map<string, CoverageSnapshot>> {\n const entityTypes = Array.from(\n new Set((batch.entityTypes ?? []).map((id) => String(id || '')).filter((id) => id.length > 0))\n )\n const result = new Map<string, CoverageSnapshot>()\n if (entityTypes.length === 0) return result\n\n const withDeleted = batch.withDeleted === true\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'entity_type' as any,\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, 'in', entityTypes)\n .where('with_deleted' as any, '=', withDeleted)\n .orderBy('refreshed_at' as any, 'desc')\n query = batch.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', batch.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', batch.organizationId ?? null)\n\n const rows = await query.execute() as Array<CoverageRow & { entity_type: string }>\n for (const row of rows ?? []) {\n const entityType = String(row.entity_type || '')\n // Rows are ordered by refreshed_at desc, so the first row seen per entity is the latest.\n if (!entityType || result.has(entityType)) continue\n const refreshedAt = row.refreshed_at instanceof Date\n ? row.refreshed_at\n : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n result.set(entityType, {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n })\n }\n return result\n}\n\nexport async function applyCoverageAdjustments(\n em: EntityManager,\n adjustments: CoverageAdjustment[],\n options?: { trx?: CoverageExecutor },\n): Promise<void> {\n if (!adjustments.length) return\n const db = (em as any).getKysely() as Kysely<any>\n // `em.getKysely()` hands back the EntityManager's transaction context whenever one is open,\n // and Kysely's `Transaction` throws rather than nesting a second one. A caller already inside\n // `em.transactional(...)` therefore gets the same guarantee the rowless nullable-tenant branch\n // opens a transaction for, so use the ambient one instead of asking for another.\n const ambientTrx = (db as { isTransaction?: boolean }).isTransaction === true\n ? (db as CoverageExecutor)\n : null\n // Keep scope initialization in a stable order. A caller-supplied transaction retains\n // advisory and unique-index locks until commit, so two batches that initialize the same\n // scopes in opposite orders could otherwise deadlock.\n const aggregated = aggregateAdjustments(adjustments).sort((left, right) => (\n scopeKey(left.scope).localeCompare(scopeKey(right.scope))\n ))\n const executor = options?.trx ?? ambientTrx\n for (const entry of aggregated) {\n const deltas = {\n deltaBase: entry.deltaBase,\n deltaIndex: entry.deltaIndex,\n deltaVector: entry.deltaVector,\n }\n if (executor) {\n await incrementCoverageRow(executor, entry.scope, deltas)\n } else if (entry.scope.tenantId == null) {\n await db.transaction().execute((trx) => incrementCoverageRow(trx, entry.scope, deltas))\n } else {\n await incrementCoverageRow(db, entry.scope, deltas)\n }\n }\n}\n\nexport async function deleteCoverageForEntity(db: Kysely<any>, entityType: string): Promise<void> {\n if (!entityType) return\n await db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', entityType)\n .execute()\n}\n\ntype CatalogueCoverageDatabase = {\n entity_index_coverage: { entity_type: string; tenant_id: string | null; organization_id: string | null }\n}\n\n/**\n * Drop every coverage row of `entityType` outside the global scope (no tenant, and the\n * organization placeholder or no organization). A declared platform-wide catalogue keeps its\n * coverage there only; rows an earlier sweep wrote under the caller's scope are never\n * refreshed again and would keep reporting stale counts.\n */\nexport async function deleteScopedCoverageForEntity(db: Kysely<CatalogueCoverageDatabase>, entityType: string): Promise<void> {\n if (!entityType) return\n await db\n .deleteFrom('entity_index_coverage')\n .where('entity_type', '=', entityType)\n .where(sql<boolean>`(tenant_id is not null or (organization_id is not null and organization_id <> ${GLOBAL_ORGANIZATION_PLACEHOLDER}))`)\n .execute()\n}\n\nasync function deleteCoverageScope(db: Kysely<any>, scope: CoverageScope): Promise<void> {\n const { entityType, tenantId, organizationId, withDeleted } = scope\n if (!entityType) return\n let query = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', entityType)\n .where('with_deleted' as any, '=', withDeleted === true)\n query = tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', tenantId)\n await applyOrganizationCondition(query as any, 'organization_id', organizationId ?? null).execute()\n}\n\nasync function tableHasColumn(db: Kysely<any>, table: string, column: string): Promise<boolean> {\n const key = `${table}.${column}`\n if (COLUMN_CACHE.has(key)) return COLUMN_CACHE.get(key)!\n const pending = COLUMN_CACHE_PENDING.get(key)\n if (pending) return pending\n const promise = (async () => {\n const exists = await db\n .selectFrom('information_schema.columns' as any)\n .select(sql<number>`1`.as('present'))\n .where(table.includes('.') ? sql<boolean>`table_schema = ${table.split('.')[0]}` : sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', table.split('.').pop())\n .where('column_name' as any, '=', column)\n .executeTakeFirst()\n const present = !!exists\n COLUMN_CACHE.set(key, present)\n return present\n })()\n COLUMN_CACHE_PENDING.set(key, promise)\n try {\n return await promise\n } finally {\n COLUMN_CACHE_PENDING.delete(key)\n }\n}\n\nexport type ColumnCheck = { table: string; column: string }\n\n// Batches the `information_schema.columns` introspection used by `refreshCoverageSnapshot`\n// into a single query for a whole set of (table, column) pairs, and pre-populates\n// `COLUMN_CACHE_PENDING` for every pair before that query even runs. Callers of\n// `coverage_warmup.ts` use this so its many concurrently-dispatched `coverage.refresh`\n// subscribers hit an already-primed (or in-flight) cache instead of each doing their own\n// per-table introspection round trip.\nexport async function primeColumnCache(db: Kysely<any>, checks: ColumnCheck[]): Promise<void> {\n for (const check of checks.filter((entry) => typeof entry?.table === 'string' && entry.table.includes('.'))) await tableHasColumn(db, check.table, check.column)\n checks = checks.filter((entry) => !(typeof entry?.table === 'string' && entry.table.includes('.')))\n const missing: Array<{ table: string; column: string; key: string }> = []\n const seen = new Set<string>()\n for (const check of checks) {\n const table = String(check?.table || '')\n const column = String(check?.column || '')\n if (!table || !column) continue\n const key = `${table}.${column}`\n if (seen.has(key)) continue\n seen.add(key)\n if (COLUMN_CACHE.has(key) || COLUMN_CACHE_PENDING.has(key)) continue\n missing.push({ table, column, key })\n }\n if (!missing.length) return\n\n const tables = Array.from(new Set(missing.map((entry) => entry.table)))\n const columns = Array.from(new Set(missing.map((entry) => entry.column)))\n\n const batchPromise = (async (): Promise<Set<string>> => {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['table_name' as any, 'column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, 'in', tables)\n .where('column_name' as any, 'in', columns)\n .execute() as Array<{ table_name: string; column_name: string }>\n return new Set(rows.map((row) => `${row.table_name}.${row.column_name}`))\n })()\n\n for (const entry of missing) {\n const entryPromise = batchPromise.then((present) => {\n const value = present.has(entry.key)\n COLUMN_CACHE.set(entry.key, value)\n return value\n })\n // Mark the stored promise as handled: when the batch query fails and no\n // `tableHasColumn` caller has adopted this entry yet (the common case \u2014 the warmup\n // awaits priming before dispatching any refresh), an orphaned rejection would\n // otherwise crash a plain-Node event worker via unhandledRejection. Awaiting\n // callers still observe the rejection through the stored reference.\n entryPromise.catch(() => undefined)\n COLUMN_CACHE_PENDING.set(entry.key, entryPromise)\n }\n\n try {\n await batchPromise\n } finally {\n for (const entry of missing) COLUMN_CACHE_PENDING.delete(entry.key)\n }\n}\n\nexport async function refreshCoverageSnapshot(\n em: EntityManager,\n scope: CoverageScope,\n): Promise<{ baseCount: number; indexedCount: number } | null> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return null\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n\n const db = (em as any).getKysely() as Kysely<any>\n const baseTable = resolveEntityTableName(em, entityType)\n\n const hasOrg = await tableHasColumn(db, baseTable, 'organization_id')\n const hasTenant = await tableHasColumn(db, baseTable, 'tenant_id')\n const hasDeleted = await tableHasColumn(db, baseTable, 'deleted_at')\n\n // A scope the base table cannot express must not narrow the index side either. Index rows\n // can carry an organization the base table has no column for \u2014 `organizations` has no\n // `organization_id` yet its index rows derive one from the record id. Filtering only the\n // index side compares two different populations and reports a gap no reindex can close;\n // returning early instead of recounting left the previous snapshot frozen, which is what\n // surfaced as a permanent \"out of sync\" row for `directory:organization`.\n //\n // Tenant is different: a base table without `tenant_id` (`user_roles`) cannot be counted\n // per tenant at all, and writing the cross-tenant total into one tenant's row would leak\n // another tenant's volume into it. Drop the unusable scoped row instead, leaving only the\n // global row \u2014 which is the one that can be true \u2014 rather than freezing a stale count.\n const scopeOrg = organizationId !== null && hasOrg\n if (tenantId !== null && !hasTenant) {\n await deleteCoverageScope(db, { entityType, tenantId, organizationId, withDeleted })\n return null\n }\n const scopeTenant = tenantId !== null && hasTenant\n\n let baseQuery = db\n .selectFrom(`${baseTable} as b` as any)\n .select(sql`count(*)`.as('count'))\n if (scopeOrg) baseQuery = baseQuery.where('b.organization_id' as any, '=', organizationId)\n if (scopeTenant) baseQuery = baseQuery.where('b.tenant_id' as any, '=', tenantId)\n if (!withDeleted && hasDeleted) baseQuery = baseQuery.where('b.deleted_at' as any, 'is', null as any)\n\n let indexQuery = db\n .selectFrom('entity_indexes as ei' as any)\n .select(sql`count(*)`.as('count'))\n .where('ei.entity_type' as any, '=', entityType)\n if (scopeOrg) indexQuery = indexQuery.where('ei.organization_id' as any, '=', organizationId)\n if (scopeTenant) indexQuery = indexQuery.where('ei.tenant_id' as any, '=', tenantId)\n if (!withDeleted) indexQuery = indexQuery.where('ei.deleted_at' as any, 'is', null as any)\n\n const vectorCountPromise = (async (): Promise<number | undefined> => {\n const hasVectorTable = await tableHasColumn(db, 'vector_search', 'entity_id')\n if (!hasVectorTable || typeof tenantId !== 'string' || tenantId.length === 0) return undefined\n\n try {\n let vectorQuery = db\n .selectFrom('vector_search' as any)\n .select(sql`count(*)`.as('count'))\n .where('entity_id' as any, '=', entityType)\n .where('tenant_id' as any, '=', tenantId)\n if (organizationId !== null) {\n vectorQuery = vectorQuery.where('organization_id' as any, '=', organizationId)\n }\n const vectorRow = await vectorQuery.executeTakeFirst() as { count: unknown } | undefined\n return toCount(vectorRow?.count)\n } catch (err) {\n logger.warn('Failed to resolve vector count for coverage snapshot', {\n entityType,\n tenantId,\n organizationId,\n error: err instanceof Error ? err.message : err,\n })\n return undefined\n }\n })()\n\n const [baseRow, indexRow, vectorCount] = await Promise.all([\n baseQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n indexQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n vectorCountPromise,\n ])\n\n const baseCount = toCount(baseRow?.count)\n const indexCount = toCount(indexRow?.count)\n\n await writeCoverageCounts(em, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorCount,\n })\n\n return { baseCount, indexedCount: indexCount }\n}\n\nexport async function writeCoverageCounts(\n em: EntityManager,\n scope: CoverageScope,\n counts: { baseCount?: number; indexedCount?: number; vectorCount?: number }\n): Promise<void> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return\n const db = (em as any).getKysely() as Kysely<any>\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n const write = async (executor: CoverageExecutor): Promise<void> => {\n if (tenantId == null) await lockCoverageInitialization(executor, scope)\n const existing = await fetchCoverageRow(executor, {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n })\n const baseCount = counts.baseCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.baseCount)))\n : Math.max(0, Math.trunc(toCount(existing?.base_count)))\n const indexCount = counts.indexedCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.indexedCount)))\n : Math.max(0, Math.trunc(toCount(existing?.indexed_count)))\n const vectorCount = counts.vectorCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.vectorCount)))\n : Math.max(0, Math.trunc(toCount(existing?.vector_indexed_count)))\n await upsertCoverageRow(executor, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorIndexedCount: vectorCount,\n })\n }\n if (tenantId == null && !db.isTransaction) {\n await db.transaction().execute(write)\n } else {\n await write(db)\n }\n}\n\ntype AggregatedAdjustment = {\n scope: CoverageScope\n deltaBase: number\n deltaIndex: number\n deltaVector: number\n}\n\nfunction aggregateAdjustments(adjustments: CoverageAdjustment[]): AggregatedAdjustment[] {\n const map = new Map<string, AggregatedAdjustment>()\n for (const adj of adjustments) {\n if (!adj?.entityType) continue\n const deltaBase = Number.isFinite(adj.deltaBase) ? adj.deltaBase : 0\n const deltaIndex = Number.isFinite(adj.deltaIndex) ? adj.deltaIndex : 0\n const deltaVector = Number.isFinite(adj.deltaVector) ? adj.deltaVector! : 0\n if (deltaBase === 0 && deltaIndex === 0 && deltaVector === 0) continue\n const scope: CoverageScope = {\n entityType: adj.entityType,\n tenantId: adj.tenantId ?? null,\n organizationId: adj.organizationId ?? null,\n withDeleted: adj.withDeleted === true,\n }\n const key = scopeKey(scope)\n const existing = map.get(key)\n if (existing) {\n existing.deltaBase += deltaBase\n existing.deltaIndex += deltaIndex\n existing.deltaVector += deltaVector\n } else {\n map.set(key, { scope, deltaBase, deltaIndex, deltaVector })\n }\n }\n return Array.from(map.values())\n}\n\nfunction scopeKey(scope: CoverageScope): string {\n const tenant = scope.tenantId ?? '__tenant_null__'\n const org = normalizeOrganizationForStore(scope.organizationId ?? null)\n const deleted = scope.withDeleted === true ? '1' : '0'\n return `${scope.entityType}|${tenant}|${org}|${deleted}`\n}\n\nexport function createCoverageAdjustments(input: CoverageDeltaInput): CoverageAdjustment[] {\n const entityType = String(input.entityType || '')\n if (!entityType) return []\n const baseDelta = Number.isFinite(input.baseDelta) ? input.baseDelta : 0\n const indexDelta = Number.isFinite(input.indexDelta) ? input.indexDelta : 0\n const vectorDelta = Number.isFinite(input.vectorDelta) ? input.vectorDelta! : 0\n if (baseDelta === 0 && indexDelta === 0 && vectorDelta === 0) return []\n const withDeleted = input.withDeleted === true\n const tenantId = input.tenantId ?? null\n const organizationId = input.organizationId ?? null\n return [\n {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n deltaBase: baseDelta,\n deltaIndex: indexDelta,\n deltaVector: vectorDelta,\n },\n ]\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7894.1.2c464c73e3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7894.1.2c464c73e3",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7894.1.2c464c73e3",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7894.1.2c464c73e3",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7894.1.2c464c73e3",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7894.1.2c464c73e3",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7894.1.2c464c73e3",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -53,10 +53,16 @@ const auditActionQuerySchema = z.object({
|
|
|
53
53
|
.default('false')
|
|
54
54
|
.describe('When `true`, only undoable actions are returned')
|
|
55
55
|
.optional(),
|
|
56
|
-
limit: z
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
limit: z
|
|
57
|
+
.string()
|
|
58
|
+
.describe('Maximum number of records to return (default 50, max 1000). Ignored when `pageSize` is sent.')
|
|
59
|
+
.optional(),
|
|
60
|
+
offset: z
|
|
61
|
+
.string()
|
|
62
|
+
.describe('Zero-based record offset for pagination (legacy — prefer page/pageSize). Takes precedence over `page` when both are sent.')
|
|
63
|
+
.optional(),
|
|
64
|
+
page: z.string().describe('Page number (default 1). Ignored when `offset` is sent.').optional(),
|
|
65
|
+
pageSize: z.string().describe('Page size (default 50, max 200). Takes precedence over `limit` when both are sent.').optional(),
|
|
60
66
|
sortField: z
|
|
61
67
|
.enum(SORT_FIELDS)
|
|
62
68
|
.describe('Sort field: `createdAt`, `user`, `action`, `field`, or `source`.')
|
|
@@ -113,18 +119,11 @@ function parseDate(value: string | null): Date | undefined {
|
|
|
113
119
|
return new Date(ts)
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
function
|
|
117
|
-
if (
|
|
118
|
-
const value = Number(param)
|
|
119
|
-
if (!Number.isFinite(value)) return 50
|
|
120
|
-
return Math.min(Math.max(Math.trunc(value), 1), 1000)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function parseOffset(param: string | null): number {
|
|
124
|
-
if (!param) return 0
|
|
122
|
+
function parseOptionalInteger(param: string | null, { min, max }: { min: number; max: number }): number | undefined {
|
|
123
|
+
if (param === null || param.trim() === '') return undefined
|
|
125
124
|
const value = Number(param)
|
|
126
|
-
if (!Number.isFinite(value)) return
|
|
127
|
-
return Math.max(Math.trunc(value),
|
|
125
|
+
if (!Number.isFinite(value)) return undefined
|
|
126
|
+
return Math.min(Math.max(Math.trunc(value), min), max)
|
|
128
127
|
}
|
|
129
128
|
|
|
130
129
|
function splitCsv(value: string | null): string[] {
|
|
@@ -141,15 +140,6 @@ function parseActionTypes(param: string | null) {
|
|
|
141
140
|
)
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
function parseNumber(param: string | null, { min, max, fallback }: { min: number; max: number; fallback: number }) {
|
|
145
|
-
if (!param) return fallback
|
|
146
|
-
const value = Number(param)
|
|
147
|
-
if (!Number.isFinite(value)) return fallback
|
|
148
|
-
const normalized = Math.trunc(value)
|
|
149
|
-
if (Number.isNaN(normalized)) return fallback
|
|
150
|
-
return Math.min(Math.max(normalized, min), max)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
143
|
export async function GET(req: Request) {
|
|
154
144
|
const auth = await getAuthFromRequest(req)
|
|
155
145
|
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
@@ -182,10 +172,10 @@ export async function GET(req: Request) {
|
|
|
182
172
|
const includeRelated = parseBooleanToken(url.searchParams.get('includeRelated')) === true
|
|
183
173
|
const includeTotal = parseBooleanToken(url.searchParams.get('includeTotal')) === true
|
|
184
174
|
const undoableOnly = parseBooleanToken(url.searchParams.get('undoableOnly')) === true
|
|
185
|
-
const limit =
|
|
186
|
-
const offset =
|
|
187
|
-
const page =
|
|
188
|
-
const pageSize =
|
|
175
|
+
const limit = parseOptionalInteger(url.searchParams.get('limit'), { min: 1, max: 1000 })
|
|
176
|
+
const offset = parseOptionalInteger(url.searchParams.get('offset'), { min: 0, max: Number.MAX_SAFE_INTEGER })
|
|
177
|
+
const page = parseOptionalInteger(url.searchParams.get('page'), { min: 1, max: 1000000 })
|
|
178
|
+
const pageSize = parseOptionalInteger(url.searchParams.get('pageSize'), { min: 1, max: 200 })
|
|
189
179
|
const sortField = SORT_FIELDS.find((value) => value === url.searchParams.get('sortField')) ?? 'createdAt'
|
|
190
180
|
const sortDir = SORT_DIRECTIONS.find((value) => value === url.searchParams.get('sortDir')) ?? 'desc'
|
|
191
181
|
const before = parseDate(url.searchParams.get('before'))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { raw, type FilterQuery } from '@mikro-orm/core'
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import { sql } from 'kysely'
|
|
4
|
+
import { z } from 'zod'
|
|
4
5
|
import { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
5
6
|
import {
|
|
6
7
|
actionLogCreateSchema,
|
|
@@ -30,6 +31,15 @@ import {
|
|
|
30
31
|
|
|
31
32
|
const logger = createLogger('audit_logs').child({ component: 'action-log-service' })
|
|
32
33
|
|
|
34
|
+
const DEFAULT_LIST_PAGE_SIZE = 50
|
|
35
|
+
|
|
36
|
+
const actionLogListQuerySchema = actionLogListSchema.extend({
|
|
37
|
+
page: z.number().int().positive().optional(),
|
|
38
|
+
pageSize: z.number().int().positive().max(200).optional(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
type ParsedActionLogListQuery = z.infer<typeof actionLogListQuerySchema>
|
|
42
|
+
|
|
33
43
|
let validationWarningLogged = false
|
|
34
44
|
let runtimeValidationAvailable: boolean | null = null
|
|
35
45
|
let decryptionWarningLogged = false
|
|
@@ -409,26 +419,26 @@ export class ActionLogService {
|
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
private parseListQuery(query: Partial<ActionLogListQuery>) {
|
|
412
|
-
return
|
|
422
|
+
return actionLogListQuerySchema.parse({
|
|
413
423
|
...query,
|
|
414
424
|
})
|
|
415
425
|
}
|
|
416
426
|
|
|
417
|
-
private resolveActorUserIds(parsed:
|
|
427
|
+
private resolveActorUserIds(parsed: ParsedActionLogListQuery): string[] {
|
|
418
428
|
const values = [...(parsed.actorUserIds ?? [])]
|
|
419
429
|
if (parsed.actorUserId) values.push(parsed.actorUserId)
|
|
420
430
|
|
|
421
431
|
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))
|
|
422
432
|
}
|
|
423
433
|
|
|
424
|
-
private resolveFieldNames(parsed:
|
|
434
|
+
private resolveFieldNames(parsed: ParsedActionLogListQuery): string[] {
|
|
425
435
|
const values = [...(parsed.fieldNames ?? [])]
|
|
426
436
|
if (parsed.fieldName) values.push(parsed.fieldName)
|
|
427
437
|
|
|
428
438
|
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))
|
|
429
439
|
}
|
|
430
440
|
|
|
431
|
-
private resolveActionTypes(parsed:
|
|
441
|
+
private resolveActionTypes(parsed: ParsedActionLogListQuery): ActionLogFilterType[] {
|
|
432
442
|
const values = [...(parsed.actionTypes ?? [])]
|
|
433
443
|
if (parsed.actionType) values.push(parsed.actionType)
|
|
434
444
|
|
|
@@ -436,22 +446,14 @@ export class ActionLogService {
|
|
|
436
446
|
.filter((value): value is ActionLogFilterType => ACTION_LOG_FILTER_TYPES.includes(value as ActionLogFilterType))
|
|
437
447
|
}
|
|
438
448
|
|
|
439
|
-
private resolvePagination(parsed:
|
|
440
|
-
const pageSize =
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
: typeof parsed.limit === 'number' && parsed.limit > 0
|
|
444
|
-
? parsed.limit
|
|
445
|
-
: 50
|
|
446
|
-
const page = typeof parsed.page === 'number' && parsed.page > 0 ? parsed.page : 1
|
|
447
|
-
const offset =
|
|
448
|
-
typeof parsed.offset === 'number' && parsed.offset >= 0
|
|
449
|
-
? parsed.offset
|
|
450
|
-
: (page - 1) * pageSize
|
|
449
|
+
private resolvePagination(parsed: ParsedActionLogListQuery): { page: number; pageSize: number; offset: number; limit: number } {
|
|
450
|
+
const pageSize = parsed.pageSize ?? parsed.limit ?? DEFAULT_LIST_PAGE_SIZE
|
|
451
|
+
const page = parsed.page ?? 1
|
|
452
|
+
const offset = parsed.offset ?? (page - 1) * pageSize
|
|
451
453
|
return { page, pageSize, offset, limit: pageSize }
|
|
452
454
|
}
|
|
453
455
|
|
|
454
|
-
private async loadEntries(parsed:
|
|
456
|
+
private async loadEntries(parsed: ParsedActionLogListQuery, options?: { paginate?: boolean }) {
|
|
455
457
|
let query = (this.buildListQuery(parsed) as any).select('action_logs.id as id')
|
|
456
458
|
|
|
457
459
|
if (options?.paginate !== false) {
|
|
@@ -480,7 +482,7 @@ export class ActionLogService {
|
|
|
480
482
|
))
|
|
481
483
|
}
|
|
482
484
|
|
|
483
|
-
private buildListQuery(parsed:
|
|
485
|
+
private buildListQuery(parsed: ParsedActionLogListQuery): any {
|
|
484
486
|
let query = (this.em.getKysely<any>() as any)
|
|
485
487
|
.selectFrom('action_logs')
|
|
486
488
|
.selectAll()
|
|
@@ -167,13 +167,14 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
|
|
|
167
167
|
<div className="space-y-2">
|
|
168
168
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
169
169
|
{visibleTiles.map((tile) => (
|
|
170
|
-
<div key={tile.id} className="group relative">
|
|
170
|
+
<div key={tile.id} data-testid="company-kpi-tile" className="group relative flex">
|
|
171
171
|
<KpiCard
|
|
172
172
|
title={tile.title}
|
|
173
173
|
value={tile.value}
|
|
174
174
|
trend={tile.trend}
|
|
175
175
|
formatValue={tile.formatValue}
|
|
176
176
|
comparisonLabel={tile.comparisonLabel}
|
|
177
|
+
className="w-full"
|
|
177
178
|
/>
|
|
178
179
|
<IconButton
|
|
179
180
|
type="button"
|