@open-mercato/core 0.4.2-canary-02d8ce2991 → 0.4.2-canary-0ba39cdeb6
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/dist/modules/configs/components/CachePanel.js +4 -4
- package/dist/modules/configs/components/CachePanel.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +48 -1
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/dashboards/services/widgetDataService.js +66 -3
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/configs/components/CachePanel.tsx +4 -4
- package/src/modules/configs/i18n/en.json +12 -2
- package/src/modules/configs/i18n/pl.json +12 -2
- package/src/modules/configs/lib/system-status.ts +48 -1
- package/src/modules/configs/lib/system-status.types.ts +1 -0
- package/src/modules/dashboards/services/widgetDataService.ts +82 -4
|
@@ -154,7 +154,7 @@ function CachePanel() {
|
|
|
154
154
|
return /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded-lg border bg-background p-6", children: [
|
|
155
155
|
/* @__PURE__ */ jsxs("header", { className: "space-y-1", children: [
|
|
156
156
|
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("configs.cache.title", "Cache overview") }),
|
|
157
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.description", "Inspect cached
|
|
157
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.description", "Inspect cached responses and clear segments when necessary.") })
|
|
158
158
|
] }),
|
|
159
159
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
160
160
|
/* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
|
|
@@ -166,7 +166,7 @@ function CachePanel() {
|
|
|
166
166
|
return /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded-lg border bg-background p-6", children: [
|
|
167
167
|
/* @__PURE__ */ jsxs("header", { className: "space-y-1", children: [
|
|
168
168
|
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("configs.cache.title", "Cache overview") }),
|
|
169
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.description", "Inspect cached
|
|
169
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.description", "Inspect cached responses and clear segments when necessary.") })
|
|
170
170
|
] }),
|
|
171
171
|
/* @__PURE__ */ jsx("div", { className: "rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: state.error }),
|
|
172
172
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleRefresh, children: t("configs.cache.retry", "Retry") }) })
|
|
@@ -178,7 +178,7 @@ function CachePanel() {
|
|
|
178
178
|
/* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between", children: [
|
|
179
179
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
180
180
|
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("configs.cache.title", "Cache overview") }),
|
|
181
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.description", "Inspect cached
|
|
181
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.description", "Inspect cached responses and clear segments when necessary.") }),
|
|
182
182
|
stats ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
183
183
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
184
184
|
"configs.cache.generatedAt",
|
|
@@ -231,7 +231,7 @@ function CachePanel() {
|
|
|
231
231
|
) }) : null
|
|
232
232
|
] }, segment.segment);
|
|
233
233
|
}) })
|
|
234
|
-
] }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.empty", "No cached
|
|
234
|
+
] }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.cache.empty", "No cached responses for this tenant.") }) })
|
|
235
235
|
] });
|
|
236
236
|
}
|
|
237
237
|
var CachePanel_default = CachePanel;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/configs/components/CachePanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst API_PATH = '/api/configs/cache'\n\ntype CrudCacheSegment = {\n segment: string\n resource: string | null\n method: string | null\n path: string | null\n keyCount: number\n}\n\ntype CrudCacheStats = {\n generatedAt: string\n totalKeys: number\n segments: CrudCacheSegment[]\n}\n\ntype FetchState = {\n loading: boolean\n error: string | null\n stats: CrudCacheStats | null\n}\n\nexport function CachePanel() {\n const t = useT()\n const [state, setState] = React.useState<FetchState>({ loading: true, error: null, stats: null })\n const [canManage, setCanManage] = React.useState(false)\n const [checkingFeature, setCheckingFeature] = React.useState(true)\n const [purgingAll, setPurgingAll] = React.useState(false)\n const [segmentPurges, setSegmentPurges] = React.useState<Record<string, boolean>>({})\n\n const loadStats = React.useCallback(async () => {\n setState((current) => ({ ...current, loading: true, error: null }))\n try {\n const stats = await readApiResultOrThrow<CrudCacheStats>(API_PATH, undefined, {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n })\n setState({ loading: false, error: null, stats })\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.loadError', 'Failed to load cache statistics.')\n setState({ loading: false, error: message, stats: null })\n }\n }, [t])\n\n React.useEffect(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n React.useEffect(() => {\n let cancelled = false\n async function checkManageFeature() {\n try {\n const payload = await readApiResultOrThrow<{ ok?: boolean; granted?: unknown }>(\n '/api/auth/feature-check',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['configs.cache.manage'] }),\n },\n {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n allowNullResult: true,\n },\n )\n if (cancelled) return\n const granted = Array.isArray(payload?.granted)\n ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]\n : []\n const hasFeature = payload?.ok === true || granted.includes('configs.cache.manage')\n setCanManage(hasFeature)\n } catch {\n if (!cancelled) setCanManage(false)\n } finally {\n if (!cancelled) setCheckingFeature(false)\n }\n }\n checkManageFeature().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const handleRefresh = React.useCallback(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n const handlePurgeAll = React.useCallback(async () => {\n if (!canManage || purgingAll) return\n if (typeof window !== 'undefined') {\n const confirmed = window.confirm(\n t('configs.cache.purgeAllConfirm', 'Purge all cached entries for this tenant?')\n )\n if (!confirmed) return\n }\n setPurgingAll(true)\n try {\n const payload = await readApiResultOrThrow<{ stats?: CrudCacheStats }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeAll' }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n )\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n flash(t('configs.cache.purgeAllSuccess', 'Cache cleared.'), 'success')\n setSegmentPurges({})\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setPurgingAll(false)\n }\n }, [canManage, purgingAll, t, handleRefresh]);\n\n\n const handlePurgeSegment = React.useCallback(async (segment: string) => {\n if (!canManage || segmentPurges[segment]) return\n if (typeof window !== 'undefined') {\n const confirmed = window.confirm(\n t('configs.cache.purgeSegmentConfirm', 'Purge cached entries for this segment?')\n )\n if (!confirmed) return\n }\n setSegmentPurges((prev) => ({ ...prev, [segment]: true }))\n try {\n const payload = await readApiResultOrThrow<{ stats?: CrudCacheStats; deleted?: number }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeSegment', segment }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n )\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n const deleted = typeof payload?.deleted === 'number' ? payload.deleted : 0\n flash(\n t('configs.cache.purgeSegmentSuccess', {\n segment,\n count: deleted,\n }),\n 'success'\n )\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setSegmentPurges((prev) => {\n const next = { ...prev }\n delete next[segment]\n return next\n })\n }\n }, [canManage, segmentPurges, t, handleRefresh]);\n\n if (state.loading) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached CRUD responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('configs.cache.loading', 'Loading cache statistics\u2026')}\n </div>\n </section>\n )\n }\n\n if (state.error) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached CRUD responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n {state.error}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" onClick={handleRefresh}>\n {t('configs.cache.retry', 'Retry')}\n </Button>\n </div>\n </section>\n )\n }\n\n const stats = state.stats\n const canShowActions = !checkingFeature && canManage\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached CRUD responses and clear segments when necessary.')}\n </p>\n {stats ? (\n <>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.generatedAt',\n 'Stats generated {{timestamp}}',\n { timestamp: new Date(stats.generatedAt).toLocaleString() }\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.totalEntries',\n '{{count}} cached entries',\n { count: stats.totalKeys }\n )}\n </p>\n </>\n ) : null}\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={handleRefresh}>\n {t('configs.cache.refresh', 'Refresh')}\n </Button>\n {canShowActions ? (\n <Button variant=\"destructive\" disabled={purgingAll} onClick={() => { void handlePurgeAll() }}>\n {purgingAll\n ? t('configs.cache.purgeAllLoading', 'Purging\u2026')\n : t('configs.cache.purgeAll', 'Purge all cache')}\n </Button>\n ) : null}\n </div>\n </header>\n <div className=\"space-y-4 rounded-lg border bg-card p-4\">\n {stats && stats.segments.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.segment', 'Segment')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.path', 'Path')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.method', 'Method')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.count', 'Cached keys')}\n </th>\n {canShowActions ? (\n <th className=\"px-3 py-2 text-right\">\n {t('configs.cache.table.actions', 'Actions')}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {stats.segments.map((segment) => {\n const isPurging = !!segmentPurges[segment.segment]\n return (\n <tr key={segment.segment} className=\"border-t\">\n <td className=\"px-3 py-2 align-top font-medium\">\n <div className=\"flex flex-col\">\n <span>{segment.segment}</span>\n {segment.resource ? (\n <span className=\"text-xs text-muted-foreground\">{segment.resource}</span>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <code className=\"text-xs text-muted-foreground\">\n {segment.path ?? t('configs.cache.table.pathUnknown', 'n/a')}\n </code>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <span className=\"text-xs uppercase text-muted-foreground\">\n {segment.method ?? 'GET'}\n </span>\n </td>\n <td className=\"px-3 py-2 align-top\">\n {t('configs.cache.table.countValue', '{{count}} keys', { count: segment.keyCount })}\n </td>\n {canShowActions ? (\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={isPurging}\n onClick={() => { void handlePurgeSegment(segment.segment) }}\n >\n {isPurging\n ? t('configs.cache.purgeSegmentLoading', 'Purging\u2026')\n : t('configs.cache.purgeSegment', 'Purge segment')}\n </Button>\n </td>\n ) : null}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.empty', 'No cached CRUD responses for this tenant.')}\n </p>\n )}\n </div>\n </section>\n )\n}\n\nexport default CachePanel\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nconst API_PATH = '/api/configs/cache'\n\ntype CrudCacheSegment = {\n segment: string\n resource: string | null\n method: string | null\n path: string | null\n keyCount: number\n}\n\ntype CrudCacheStats = {\n generatedAt: string\n totalKeys: number\n segments: CrudCacheSegment[]\n}\n\ntype FetchState = {\n loading: boolean\n error: string | null\n stats: CrudCacheStats | null\n}\n\nexport function CachePanel() {\n const t = useT()\n const [state, setState] = React.useState<FetchState>({ loading: true, error: null, stats: null })\n const [canManage, setCanManage] = React.useState(false)\n const [checkingFeature, setCheckingFeature] = React.useState(true)\n const [purgingAll, setPurgingAll] = React.useState(false)\n const [segmentPurges, setSegmentPurges] = React.useState<Record<string, boolean>>({})\n\n const loadStats = React.useCallback(async () => {\n setState((current) => ({ ...current, loading: true, error: null }))\n try {\n const stats = await readApiResultOrThrow<CrudCacheStats>(API_PATH, undefined, {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n })\n setState({ loading: false, error: null, stats })\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.loadError', 'Failed to load cache statistics.')\n setState({ loading: false, error: message, stats: null })\n }\n }, [t])\n\n React.useEffect(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n React.useEffect(() => {\n let cancelled = false\n async function checkManageFeature() {\n try {\n const payload = await readApiResultOrThrow<{ ok?: boolean; granted?: unknown }>(\n '/api/auth/feature-check',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['configs.cache.manage'] }),\n },\n {\n errorMessage: t('configs.cache.loadError', 'Failed to load cache statistics.'),\n allowNullResult: true,\n },\n )\n if (cancelled) return\n const granted = Array.isArray(payload?.granted)\n ? (payload.granted as unknown[]).filter((feature) => typeof feature === 'string') as string[]\n : []\n const hasFeature = payload?.ok === true || granted.includes('configs.cache.manage')\n setCanManage(hasFeature)\n } catch {\n if (!cancelled) setCanManage(false)\n } finally {\n if (!cancelled) setCheckingFeature(false)\n }\n }\n checkManageFeature().catch(() => {})\n return () => {\n cancelled = true\n }\n }, [t])\n\n const handleRefresh = React.useCallback(() => {\n loadStats().catch(() => {})\n }, [loadStats])\n\n const handlePurgeAll = React.useCallback(async () => {\n if (!canManage || purgingAll) return\n if (typeof window !== 'undefined') {\n const confirmed = window.confirm(\n t('configs.cache.purgeAllConfirm', 'Purge all cached entries for this tenant?')\n )\n if (!confirmed) return\n }\n setPurgingAll(true)\n try {\n const payload = await readApiResultOrThrow<{ stats?: CrudCacheStats }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeAll' }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n )\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n flash(t('configs.cache.purgeAllSuccess', 'Cache cleared.'), 'success')\n setSegmentPurges({})\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setPurgingAll(false)\n }\n }, [canManage, purgingAll, t, handleRefresh]);\n\n\n const handlePurgeSegment = React.useCallback(async (segment: string) => {\n if (!canManage || segmentPurges[segment]) return\n if (typeof window !== 'undefined') {\n const confirmed = window.confirm(\n t('configs.cache.purgeSegmentConfirm', 'Purge cached entries for this segment?')\n )\n if (!confirmed) return\n }\n setSegmentPurges((prev) => ({ ...prev, [segment]: true }))\n try {\n const payload = await readApiResultOrThrow<{ stats?: CrudCacheStats; deleted?: number }>(\n API_PATH,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action: 'purgeSegment', segment }),\n },\n {\n errorMessage: t('configs.cache.purgeError', 'Failed to purge cache segment.'),\n allowNullResult: true,\n },\n )\n const stats = payload?.stats\n if (stats) {\n setState({ loading: false, error: null, stats })\n } else {\n handleRefresh()\n }\n const deleted = typeof payload?.deleted === 'number' ? payload.deleted : 0\n flash(\n t('configs.cache.purgeSegmentSuccess', {\n segment,\n count: deleted,\n }),\n 'success'\n )\n } catch (error) {\n const message =\n error instanceof Error && error.message\n ? error.message\n : t('configs.cache.purgeError', 'Failed to purge cache segment.')\n flash(message, 'error')\n } finally {\n setSegmentPurges((prev) => {\n const next = { ...prev }\n delete next[segment]\n return next\n })\n }\n }, [canManage, segmentPurges, t, handleRefresh]);\n\n if (state.loading) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {t('configs.cache.loading', 'Loading cache statistics\u2026')}\n </div>\n </section>\n )\n }\n\n if (state.error) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-6\">\n <header className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n </header>\n <div className=\"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n {state.error}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" onClick={handleRefresh}>\n {t('configs.cache.retry', 'Retry')}\n </Button>\n </div>\n </section>\n )\n }\n\n const stats = state.stats\n const canShowActions = !checkingFeature && canManage\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('configs.cache.title', 'Cache overview')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}\n </p>\n {stats ? (\n <>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.generatedAt',\n 'Stats generated {{timestamp}}',\n { timestamp: new Date(stats.generatedAt).toLocaleString() }\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'configs.cache.totalEntries',\n '{{count}} cached entries',\n { count: stats.totalKeys }\n )}\n </p>\n </>\n ) : null}\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={handleRefresh}>\n {t('configs.cache.refresh', 'Refresh')}\n </Button>\n {canShowActions ? (\n <Button variant=\"destructive\" disabled={purgingAll} onClick={() => { void handlePurgeAll() }}>\n {purgingAll\n ? t('configs.cache.purgeAllLoading', 'Purging\u2026')\n : t('configs.cache.purgeAll', 'Purge all cache')}\n </Button>\n ) : null}\n </div>\n </header>\n <div className=\"space-y-4 rounded-lg border bg-card p-4\">\n {stats && stats.segments.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.segment', 'Segment')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.path', 'Path')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.method', 'Method')}\n </th>\n <th className=\"px-3 py-2 text-left\">\n {t('configs.cache.table.count', 'Cached keys')}\n </th>\n {canShowActions ? (\n <th className=\"px-3 py-2 text-right\">\n {t('configs.cache.table.actions', 'Actions')}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody>\n {stats.segments.map((segment) => {\n const isPurging = !!segmentPurges[segment.segment]\n return (\n <tr key={segment.segment} className=\"border-t\">\n <td className=\"px-3 py-2 align-top font-medium\">\n <div className=\"flex flex-col\">\n <span>{segment.segment}</span>\n {segment.resource ? (\n <span className=\"text-xs text-muted-foreground\">{segment.resource}</span>\n ) : null}\n </div>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <code className=\"text-xs text-muted-foreground\">\n {segment.path ?? t('configs.cache.table.pathUnknown', 'n/a')}\n </code>\n </td>\n <td className=\"px-3 py-2 align-top\">\n <span className=\"text-xs uppercase text-muted-foreground\">\n {segment.method ?? 'GET'}\n </span>\n </td>\n <td className=\"px-3 py-2 align-top\">\n {t('configs.cache.table.countValue', '{{count}} keys', { count: segment.keyCount })}\n </td>\n {canShowActions ? (\n <td className=\"px-3 py-2 align-top text-right\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={isPurging}\n onClick={() => { void handlePurgeSegment(segment.segment) }}\n >\n {isPurging\n ? t('configs.cache.purgeSegmentLoading', 'Purging\u2026')\n : t('configs.cache.purgeSegment', 'Purge segment')}\n </Button>\n </td>\n ) : null}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('configs.cache.empty', 'No cached responses for this tenant.')}\n </p>\n )}\n </div>\n </section>\n )\n}\n\nexport default CachePanel\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["stats"]
|
|
7
7
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
2
|
-
const CATEGORY_ORDER = [
|
|
2
|
+
const CATEGORY_ORDER = [
|
|
3
|
+
"profiling",
|
|
4
|
+
"logging",
|
|
5
|
+
"security",
|
|
6
|
+
"caching",
|
|
7
|
+
"query_index",
|
|
8
|
+
"entities"
|
|
9
|
+
];
|
|
3
10
|
const CATEGORY_METADATA = {
|
|
4
11
|
profiling: {
|
|
5
12
|
labelKey: "configs.systemStatus.categories.profiling",
|
|
@@ -9,6 +16,10 @@ const CATEGORY_METADATA = {
|
|
|
9
16
|
labelKey: "configs.systemStatus.categories.logging",
|
|
10
17
|
descriptionKey: "configs.systemStatus.categories.loggingDescription"
|
|
11
18
|
},
|
|
19
|
+
security: {
|
|
20
|
+
labelKey: "configs.systemStatus.categories.security",
|
|
21
|
+
descriptionKey: "configs.systemStatus.categories.securityDescription"
|
|
22
|
+
},
|
|
12
23
|
caching: {
|
|
13
24
|
labelKey: "configs.systemStatus.categories.caching",
|
|
14
25
|
descriptionKey: "configs.systemStatus.categories.cachingDescription"
|
|
@@ -87,6 +98,42 @@ const SYSTEM_STATUS_VARIABLES = [
|
|
|
87
98
|
docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,
|
|
88
99
|
defaultValue: ""
|
|
89
100
|
},
|
|
101
|
+
{
|
|
102
|
+
key: "OM_PASSWORD_MIN_LENGTH",
|
|
103
|
+
category: "security",
|
|
104
|
+
kind: "string",
|
|
105
|
+
labelKey: "configs.systemStatus.variables.passwordMinLength.label",
|
|
106
|
+
descriptionKey: "configs.systemStatus.variables.passwordMinLength.description",
|
|
107
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_min_length`,
|
|
108
|
+
defaultValue: "6"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "OM_PASSWORD_REQUIRE_DIGIT",
|
|
112
|
+
category: "security",
|
|
113
|
+
kind: "boolean",
|
|
114
|
+
labelKey: "configs.systemStatus.variables.passwordRequireDigit.label",
|
|
115
|
+
descriptionKey: "configs.systemStatus.variables.passwordRequireDigit.description",
|
|
116
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_digit`,
|
|
117
|
+
defaultValue: "true"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: "OM_PASSWORD_REQUIRE_UPPERCASE",
|
|
121
|
+
category: "security",
|
|
122
|
+
kind: "boolean",
|
|
123
|
+
labelKey: "configs.systemStatus.variables.passwordRequireUppercase.label",
|
|
124
|
+
descriptionKey: "configs.systemStatus.variables.passwordRequireUppercase.description",
|
|
125
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_uppercase`,
|
|
126
|
+
defaultValue: "true"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: "OM_PASSWORD_REQUIRE_SPECIAL",
|
|
130
|
+
category: "security",
|
|
131
|
+
kind: "boolean",
|
|
132
|
+
labelKey: "configs.systemStatus.variables.passwordRequireSpecial.label",
|
|
133
|
+
descriptionKey: "configs.systemStatus.variables.passwordRequireSpecial.description",
|
|
134
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_special`,
|
|
135
|
+
defaultValue: "true"
|
|
136
|
+
},
|
|
90
137
|
{
|
|
91
138
|
key: "ENABLE_CRUD_API_CACHE",
|
|
92
139
|
category: "caching",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/configs/lib/system-status.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n SystemStatusCategory,\n SystemStatusCategoryKey,\n SystemStatusItem,\n SystemStatusSnapshot,\n SystemStatusVariableKind,\n SystemStatusState,\n SystemStatusRuntimeMode,\n} from './system-status.types'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype SystemStatusVariableDefinition = {\n key: string\n category: SystemStatusCategoryKey\n kind: SystemStatusVariableKind\n labelKey: string\n descriptionKey: string\n docUrl: string | null\n defaultValue: string | null\n}\n\nconst CATEGORY_ORDER: SystemStatusCategoryKey[] = ['profiling', 'logging', 'caching', 'query_index', 'entities']\n\nconst CATEGORY_METADATA: Record<\n SystemStatusCategoryKey,\n { labelKey: string; descriptionKey: string | null }\n> = {\n profiling: {\n labelKey: 'configs.systemStatus.categories.profiling',\n descriptionKey: 'configs.systemStatus.categories.profilingDescription',\n },\n logging: {\n labelKey: 'configs.systemStatus.categories.logging',\n descriptionKey: 'configs.systemStatus.categories.loggingDescription',\n },\n caching: {\n labelKey: 'configs.systemStatus.categories.caching',\n descriptionKey: 'configs.systemStatus.categories.cachingDescription',\n },\n query_index: {\n labelKey: 'configs.systemStatus.categories.queryIndex',\n descriptionKey: 'configs.systemStatus.categories.queryIndexDescription',\n },\n entities: {\n labelKey: 'configs.systemStatus.categories.entities',\n descriptionKey: 'configs.systemStatus.categories.entitiesDescription',\n },\n}\n\nconst SYSTEM_STATUS_DOC_BASE = 'https://docs.openmercato.com/docs/framework/operations/system-status'\n\nexport const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [\n {\n key: 'OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_profile`,\n defaultValue: '',\n },\n {\n key: 'NEXT_PUBLIC_OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.nextPublicOmProfile.label',\n descriptionKey: 'configs.systemStatus.variables.nextPublicOmProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#next_public_om_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_CRUD_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omCrudProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omCrudProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_crud_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_QE_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omQeProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omQeProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_qe_profile`,\n defaultValue: '',\n },\n {\n key: 'QUERY_ENGINE_DEBUG_SQL',\n category: 'logging',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.queryEngineDebugSql.label',\n descriptionKey: 'configs.systemStatus.variables.queryEngineDebugSql.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_engine_debug_sql`,\n defaultValue: 'false',\n },\n {\n key: 'LOG_VERBOSITY',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logVerbosity.label',\n descriptionKey: 'configs.systemStatus.variables.logVerbosity.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_verbosity`,\n defaultValue: '',\n },\n {\n key: 'LOG_LEVEL',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logLevel.label',\n descriptionKey: 'configs.systemStatus.variables.logLevel.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,\n defaultValue: '',\n },\n {\n key: 'ENABLE_CRUD_API_CACHE',\n category: 'caching',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.enableCrudApiCache.label',\n descriptionKey: 'configs.systemStatus.variables.enableCrudApiCache.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#enable_crud_api_cache`,\n defaultValue: 'false',\n },\n {\n key: 'CACHE_STRATEGY',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheStrategy.label',\n descriptionKey: 'configs.systemStatus.variables.cacheStrategy.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_strategy`,\n defaultValue: 'memory',\n },\n {\n key: 'CACHE_TTL',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheTtl.label',\n descriptionKey: 'configs.systemStatus.variables.cacheTtl.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_ttl`,\n defaultValue: '',\n },\n {\n key: 'CACHE_SQLITE_PATH',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheSqlitePath.label',\n descriptionKey: 'configs.systemStatus.variables.cacheSqlitePath.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_sqlite_path`,\n defaultValue: './data/cache.db',\n },\n {\n key: 'SCHEDULE_AUTO_REINDEX',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.scheduleAutoReindex.label',\n descriptionKey: 'configs.systemStatus.variables.scheduleAutoReindex.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#schedule_auto_reindex`,\n defaultValue: 'true',\n },\n {\n key: 'OPTIMIZE_INDEX_COVERAGE_STATS',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.label',\n descriptionKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#optimize_index_coverage_stats`,\n defaultValue: 'false',\n },\n {\n key: 'FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.label',\n descriptionKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#force_query_index_on_partial_indexes`,\n defaultValue: 'true',\n },\n {\n key: 'ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM',\n category: 'entities',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.entitiesBackcompatEav.label',\n descriptionKey: 'configs.systemStatus.variables.entitiesBackcompatEav.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#entities_backcompat_eav_for_custom`,\n defaultValue: 'false',\n },\n]\n\ntype AnalyzedValue = { state: SystemStatusState; value: string | null; normalizedValue: string | null }\n\nfunction analyzeBooleanValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n const parsed = parseBooleanToken(trimmed)\n if (parsed === true) {\n return { state: 'enabled', value: trimmed, normalizedValue: 'true' }\n }\n if (parsed === false) {\n return { state: 'disabled', value: trimmed, normalizedValue: 'false' }\n }\n return { state: 'unknown', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction analyzeStringValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n return { state: 'set', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction toItem(definition: SystemStatusVariableDefinition, env: Record<string, string | undefined>): SystemStatusItem {\n const raw = env[definition.key]\n const analyzed = definition.kind === 'boolean' ? analyzeBooleanValue(raw) : analyzeStringValue(raw)\n return {\n key: definition.key,\n category: definition.category,\n kind: definition.kind,\n labelKey: definition.labelKey,\n descriptionKey: definition.descriptionKey,\n docUrl: definition.docUrl,\n defaultValue: definition.defaultValue,\n state: analyzed.state,\n value: analyzed.value,\n normalizedValue: analyzed.normalizedValue,\n }\n}\n\nfunction buildCategorySnapshot(\n key: SystemStatusCategoryKey,\n items: SystemStatusItem[],\n): SystemStatusCategory {\n const metadata = CATEGORY_METADATA[key]\n return {\n key,\n labelKey: metadata.labelKey,\n descriptionKey: metadata.descriptionKey,\n items,\n }\n}\n\nfunction resolveRuntimeMode(env: Record<string, string | undefined>): SystemStatusRuntimeMode {\n const raw = env.NODE_ENV\n if (typeof raw !== 'string') return 'unknown'\n const value = raw.trim().toLowerCase()\n if (value === 'development') return 'development'\n if (value === 'production') return 'production'\n if (value === 'test') return 'test'\n return 'unknown'\n}\n\nexport function buildSystemStatusSnapshot(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): SystemStatusSnapshot {\n const byCategory = new Map<SystemStatusCategoryKey, SystemStatusItem[]>()\n for (const definition of SYSTEM_STATUS_VARIABLES) {\n const bucket = byCategory.get(definition.category)\n const item = toItem(definition, env)\n if (bucket) {\n bucket.push(item)\n } else {\n byCategory.set(definition.category, [item])\n }\n }\n\n const categories: SystemStatusCategory[] = []\n for (const categoryKey of CATEGORY_ORDER) {\n const items = byCategory.get(categoryKey) ?? []\n categories.push(buildCategorySnapshot(categoryKey, items))\n }\n\n return {\n generatedAt: new Date().toISOString(),\n runtimeMode: resolveRuntimeMode(env),\n categories,\n }\n}\n"],
|
|
5
|
-
"mappings": "AASA,SAAS,yBAAyB;AAYlC,MAAM,iBAA4C,
|
|
4
|
+
"sourcesContent": ["import type {\n SystemStatusCategory,\n SystemStatusCategoryKey,\n SystemStatusItem,\n SystemStatusSnapshot,\n SystemStatusVariableKind,\n SystemStatusState,\n SystemStatusRuntimeMode,\n} from './system-status.types'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype SystemStatusVariableDefinition = {\n key: string\n category: SystemStatusCategoryKey\n kind: SystemStatusVariableKind\n labelKey: string\n descriptionKey: string\n docUrl: string | null\n defaultValue: string | null\n}\n\nconst CATEGORY_ORDER: SystemStatusCategoryKey[] = [\n 'profiling',\n 'logging',\n 'security',\n 'caching',\n 'query_index',\n 'entities',\n]\n\nconst CATEGORY_METADATA: Record<\n SystemStatusCategoryKey,\n { labelKey: string; descriptionKey: string | null }\n> = {\n profiling: {\n labelKey: 'configs.systemStatus.categories.profiling',\n descriptionKey: 'configs.systemStatus.categories.profilingDescription',\n },\n logging: {\n labelKey: 'configs.systemStatus.categories.logging',\n descriptionKey: 'configs.systemStatus.categories.loggingDescription',\n },\n security: {\n labelKey: 'configs.systemStatus.categories.security',\n descriptionKey: 'configs.systemStatus.categories.securityDescription',\n },\n caching: {\n labelKey: 'configs.systemStatus.categories.caching',\n descriptionKey: 'configs.systemStatus.categories.cachingDescription',\n },\n query_index: {\n labelKey: 'configs.systemStatus.categories.queryIndex',\n descriptionKey: 'configs.systemStatus.categories.queryIndexDescription',\n },\n entities: {\n labelKey: 'configs.systemStatus.categories.entities',\n descriptionKey: 'configs.systemStatus.categories.entitiesDescription',\n },\n}\n\nconst SYSTEM_STATUS_DOC_BASE = 'https://docs.openmercato.com/docs/framework/operations/system-status'\n\nexport const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [\n {\n key: 'OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_profile`,\n defaultValue: '',\n },\n {\n key: 'NEXT_PUBLIC_OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.nextPublicOmProfile.label',\n descriptionKey: 'configs.systemStatus.variables.nextPublicOmProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#next_public_om_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_CRUD_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omCrudProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omCrudProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_crud_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_QE_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omQeProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omQeProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_qe_profile`,\n defaultValue: '',\n },\n {\n key: 'QUERY_ENGINE_DEBUG_SQL',\n category: 'logging',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.queryEngineDebugSql.label',\n descriptionKey: 'configs.systemStatus.variables.queryEngineDebugSql.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_engine_debug_sql`,\n defaultValue: 'false',\n },\n {\n key: 'LOG_VERBOSITY',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logVerbosity.label',\n descriptionKey: 'configs.systemStatus.variables.logVerbosity.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_verbosity`,\n defaultValue: '',\n },\n {\n key: 'LOG_LEVEL',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logLevel.label',\n descriptionKey: 'configs.systemStatus.variables.logLevel.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,\n defaultValue: '',\n },\n {\n key: 'OM_PASSWORD_MIN_LENGTH',\n category: 'security',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.passwordMinLength.label',\n descriptionKey: 'configs.systemStatus.variables.passwordMinLength.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_min_length`,\n defaultValue: '6',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_DIGIT',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireDigit.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireDigit.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_digit`,\n defaultValue: 'true',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_UPPERCASE',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireUppercase.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireUppercase.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_uppercase`,\n defaultValue: 'true',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_SPECIAL',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireSpecial.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireSpecial.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_special`,\n defaultValue: 'true',\n },\n {\n key: 'ENABLE_CRUD_API_CACHE',\n category: 'caching',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.enableCrudApiCache.label',\n descriptionKey: 'configs.systemStatus.variables.enableCrudApiCache.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#enable_crud_api_cache`,\n defaultValue: 'false',\n },\n {\n key: 'CACHE_STRATEGY',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheStrategy.label',\n descriptionKey: 'configs.systemStatus.variables.cacheStrategy.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_strategy`,\n defaultValue: 'memory',\n },\n {\n key: 'CACHE_TTL',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheTtl.label',\n descriptionKey: 'configs.systemStatus.variables.cacheTtl.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_ttl`,\n defaultValue: '',\n },\n {\n key: 'CACHE_SQLITE_PATH',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheSqlitePath.label',\n descriptionKey: 'configs.systemStatus.variables.cacheSqlitePath.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_sqlite_path`,\n defaultValue: './data/cache.db',\n },\n {\n key: 'SCHEDULE_AUTO_REINDEX',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.scheduleAutoReindex.label',\n descriptionKey: 'configs.systemStatus.variables.scheduleAutoReindex.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#schedule_auto_reindex`,\n defaultValue: 'true',\n },\n {\n key: 'OPTIMIZE_INDEX_COVERAGE_STATS',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.label',\n descriptionKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#optimize_index_coverage_stats`,\n defaultValue: 'false',\n },\n {\n key: 'FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.label',\n descriptionKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#force_query_index_on_partial_indexes`,\n defaultValue: 'true',\n },\n {\n key: 'ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM',\n category: 'entities',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.entitiesBackcompatEav.label',\n descriptionKey: 'configs.systemStatus.variables.entitiesBackcompatEav.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#entities_backcompat_eav_for_custom`,\n defaultValue: 'false',\n },\n]\n\ntype AnalyzedValue = { state: SystemStatusState; value: string | null; normalizedValue: string | null }\n\nfunction analyzeBooleanValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n const parsed = parseBooleanToken(trimmed)\n if (parsed === true) {\n return { state: 'enabled', value: trimmed, normalizedValue: 'true' }\n }\n if (parsed === false) {\n return { state: 'disabled', value: trimmed, normalizedValue: 'false' }\n }\n return { state: 'unknown', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction analyzeStringValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n return { state: 'set', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction toItem(definition: SystemStatusVariableDefinition, env: Record<string, string | undefined>): SystemStatusItem {\n const raw = env[definition.key]\n const analyzed = definition.kind === 'boolean' ? analyzeBooleanValue(raw) : analyzeStringValue(raw)\n return {\n key: definition.key,\n category: definition.category,\n kind: definition.kind,\n labelKey: definition.labelKey,\n descriptionKey: definition.descriptionKey,\n docUrl: definition.docUrl,\n defaultValue: definition.defaultValue,\n state: analyzed.state,\n value: analyzed.value,\n normalizedValue: analyzed.normalizedValue,\n }\n}\n\nfunction buildCategorySnapshot(\n key: SystemStatusCategoryKey,\n items: SystemStatusItem[],\n): SystemStatusCategory {\n const metadata = CATEGORY_METADATA[key]\n return {\n key,\n labelKey: metadata.labelKey,\n descriptionKey: metadata.descriptionKey,\n items,\n }\n}\n\nfunction resolveRuntimeMode(env: Record<string, string | undefined>): SystemStatusRuntimeMode {\n const raw = env.NODE_ENV\n if (typeof raw !== 'string') return 'unknown'\n const value = raw.trim().toLowerCase()\n if (value === 'development') return 'development'\n if (value === 'production') return 'production'\n if (value === 'test') return 'test'\n return 'unknown'\n}\n\nexport function buildSystemStatusSnapshot(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): SystemStatusSnapshot {\n const byCategory = new Map<SystemStatusCategoryKey, SystemStatusItem[]>()\n for (const definition of SYSTEM_STATUS_VARIABLES) {\n const bucket = byCategory.get(definition.category)\n const item = toItem(definition, env)\n if (bucket) {\n bucket.push(item)\n } else {\n byCategory.set(definition.category, [item])\n }\n }\n\n const categories: SystemStatusCategory[] = []\n for (const categoryKey of CATEGORY_ORDER) {\n const items = byCategory.get(categoryKey) ?? []\n categories.push(buildCategorySnapshot(categoryKey, items))\n }\n\n return {\n generatedAt: new Date().toISOString(),\n runtimeMode: resolveRuntimeMode(env),\n categories,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { decryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
|
|
2
3
|
import { resolveTenantEncryptionService } from "@open-mercato/shared/lib/encryption/customFieldValues";
|
|
3
4
|
import { resolveEntityIdFromMetadata } from "@open-mercato/shared/lib/encryption/entityIds";
|
|
5
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
6
|
import {
|
|
5
7
|
resolveDateRange,
|
|
6
8
|
getPreviousPeriod,
|
|
@@ -169,36 +171,82 @@ class WidgetDataService {
|
|
|
169
171
|
assertSafeIdentifier(config.table, "table name");
|
|
170
172
|
assertSafeIdentifier(config.idColumn, "id column");
|
|
171
173
|
assertSafeIdentifier(config.labelColumn, "label column");
|
|
174
|
+
const meta = this.resolveEntityMetadata(config.table);
|
|
175
|
+
const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null;
|
|
176
|
+
const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null;
|
|
177
|
+
const tenantProp = meta ? this.resolveEntityPropertyName(meta, "tenant_id") ?? this.resolveEntityPropertyName(meta, "tenantId") : null;
|
|
178
|
+
const organizationProp = meta ? this.resolveEntityPropertyName(meta, "organization_id") ?? this.resolveEntityPropertyName(meta, "organizationId") : null;
|
|
179
|
+
const entityName = meta ? meta.class ?? meta.className ?? meta.name : null;
|
|
180
|
+
if (meta && idProp && labelProp && tenantProp && entityName) {
|
|
181
|
+
const where = {
|
|
182
|
+
[idProp]: { $in: uniqueIds },
|
|
183
|
+
[tenantProp]: this.scope.tenantId
|
|
184
|
+
};
|
|
185
|
+
if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {
|
|
186
|
+
where[organizationProp] = { $in: this.scope.organizationIds };
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const records = await findWithDecryption(
|
|
190
|
+
this.em,
|
|
191
|
+
entityName,
|
|
192
|
+
where,
|
|
193
|
+
{ fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },
|
|
194
|
+
{ tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() }
|
|
195
|
+
);
|
|
196
|
+
const labelMap = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const record of records) {
|
|
198
|
+
const id = record[idProp];
|
|
199
|
+
const label = record[labelProp];
|
|
200
|
+
if (typeof id === "string" && label != null && label !== "") {
|
|
201
|
+
labelMap.set(id, String(label));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (labelMap.size > 0) {
|
|
205
|
+
return data.map((item) => ({
|
|
206
|
+
...item,
|
|
207
|
+
groupLabel: typeof item.groupKey === "string" && labelMap.has(item.groupKey) ? labelMap.get(item.groupKey) : void 0
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
172
213
|
const clauses = [`"${config.idColumn}" = ANY(?::uuid[])`, "tenant_id = ?"];
|
|
173
214
|
const params = [`{${uniqueIds.join(",")}}`, this.scope.tenantId];
|
|
174
215
|
if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {
|
|
175
216
|
clauses.push("organization_id = ANY(?::uuid[])");
|
|
176
217
|
params.push(`{${this.scope.organizationIds.join(",")}}`);
|
|
177
218
|
}
|
|
178
|
-
const sql = `SELECT "${config.idColumn}" as id, "${config.labelColumn}" as label FROM "${config.table}" WHERE ${clauses.join(
|
|
219
|
+
const sql = `SELECT "${config.idColumn}" as id, "${config.labelColumn}" as label, tenant_id, organization_id FROM "${config.table}" WHERE ${clauses.join(
|
|
179
220
|
" AND "
|
|
180
221
|
)}`;
|
|
181
222
|
try {
|
|
182
223
|
const labelRows = await this.em.getConnection().execute(sql, params);
|
|
183
|
-
const meta = this.resolveEntityMetadata(config.table);
|
|
184
224
|
const entityId = this.resolveEntityId(meta);
|
|
185
225
|
const encryptionService = resolveTenantEncryptionService(this.em);
|
|
186
226
|
const organizationId = this.resolveOrganizationId();
|
|
227
|
+
const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null;
|
|
187
228
|
const labelMap = /* @__PURE__ */ new Map();
|
|
188
229
|
for (const row of labelRows) {
|
|
189
230
|
let labelValue = row.label;
|
|
190
231
|
if (entityId && encryptionService?.isEnabled() && labelValue != null) {
|
|
232
|
+
const rowOrgId = row.organization_id ?? organizationId ?? null;
|
|
191
233
|
const decrypted = await encryptionService.decryptEntityPayload(
|
|
192
234
|
entityId,
|
|
193
235
|
{ [config.labelColumn]: labelValue },
|
|
194
236
|
this.scope.tenantId,
|
|
195
|
-
|
|
237
|
+
rowOrgId
|
|
196
238
|
);
|
|
197
239
|
const resolved = decrypted[config.labelColumn];
|
|
198
240
|
if (typeof resolved === "string" || typeof resolved === "number") {
|
|
199
241
|
labelValue = String(resolved);
|
|
200
242
|
}
|
|
201
243
|
}
|
|
244
|
+
if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {
|
|
245
|
+
const decrypted = decryptWithAesGcm(labelValue, dek.key);
|
|
246
|
+
if (decrypted !== null) {
|
|
247
|
+
labelValue = decrypted;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
202
250
|
if (row.id && labelValue != null && labelValue !== "") {
|
|
203
251
|
labelMap.set(row.id, labelValue);
|
|
204
252
|
}
|
|
@@ -231,6 +279,17 @@ class WidgetDataService {
|
|
|
231
279
|
});
|
|
232
280
|
return match ?? null;
|
|
233
281
|
}
|
|
282
|
+
resolveEntityPropertyName(meta, columnName) {
|
|
283
|
+
const properties = meta?.properties ? Object.values(meta.properties) : [];
|
|
284
|
+
for (const prop of properties) {
|
|
285
|
+
const fieldName = prop?.fieldName;
|
|
286
|
+
const fieldNames = prop?.fieldNames;
|
|
287
|
+
if (typeof fieldName === "string" && fieldName === columnName) return prop?.name ?? null;
|
|
288
|
+
if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null;
|
|
289
|
+
if (prop?.name === columnName) return prop?.name ?? null;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
234
293
|
resolveEntityId(meta) {
|
|
235
294
|
if (!meta) return null;
|
|
236
295
|
try {
|
|
@@ -239,6 +298,10 @@ class WidgetDataService {
|
|
|
239
298
|
return null;
|
|
240
299
|
}
|
|
241
300
|
}
|
|
301
|
+
isEncryptedPayload(value) {
|
|
302
|
+
const parts = value.split(":");
|
|
303
|
+
return parts.length === 4 && parts[3] === "v1";
|
|
304
|
+
}
|
|
242
305
|
}
|
|
243
306
|
function createWidgetDataService(em, scope, registry, cache) {
|
|
244
307
|
return new WidgetDataService({ em, scope, registry, cache });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dashboards/services/widgetDataService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n} from '../lib/aggregations'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const mainResult = await this.executeQuery(request, dateRangeResolved)\n\n let comparisonResult: { value: number | null; data: WidgetDataItem[] } | undefined\n if (comparisonRange && request.dateRange) {\n comparisonResult = await this.executeQuery(request, comparisonRange)\n }\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => ({\n ...item,\n groupLabel: item.groupKey != null && item.groupKey !== '' ? String(item.groupKey) : undefined,\n }))\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const meta = this.resolveEntityMetadata(config.table)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n organizationId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n} from '../lib/aggregations'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const mainResult = await this.executeQuery(request, dateRangeResolved)\n\n let comparisonResult: { value: number | null; data: WidgetDataItem[] } | undefined\n if (comparisonRange && request.dateRange) {\n comparisonResult = await this.executeQuery(request, comparisonRange)\n }\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => ({\n ...item,\n groupLabel: item.groupKey != null && item.groupKey !== '' ? String(item.groupKey) : undefined,\n }))\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n const label = record[labelProp]\n if (typeof id === 'string' && label != null && label !== '') {\n labelMap.set(id, String(label))\n }\n }\n\n if (labelMap.size > 0) {\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n }\n } catch {\n // fall through to SQL resolution\n }\n }\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = decryptWithAesGcm(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\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.4.2-canary-
|
|
3
|
+
"version": "0.4.2-canary-0ba39cdeb6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.2-canary-
|
|
210
|
+
"@open-mercato/shared": "0.4.2-canary-0ba39cdeb6",
|
|
211
211
|
"@xyflow/react": "^12.6.0",
|
|
212
212
|
"date-fns": "^4.1.0",
|
|
213
213
|
"date-fns-tz": "^3.2.0"
|
|
@@ -194,7 +194,7 @@ export function CachePanel() {
|
|
|
194
194
|
<header className="space-y-1">
|
|
195
195
|
<h2 className="text-lg font-semibold">{t('configs.cache.title', 'Cache overview')}</h2>
|
|
196
196
|
<p className="text-sm text-muted-foreground">
|
|
197
|
-
{t('configs.cache.description', 'Inspect cached
|
|
197
|
+
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
198
198
|
</p>
|
|
199
199
|
</header>
|
|
200
200
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
@@ -211,7 +211,7 @@ export function CachePanel() {
|
|
|
211
211
|
<header className="space-y-1">
|
|
212
212
|
<h2 className="text-lg font-semibold">{t('configs.cache.title', 'Cache overview')}</h2>
|
|
213
213
|
<p className="text-sm text-muted-foreground">
|
|
214
|
-
{t('configs.cache.description', 'Inspect cached
|
|
214
|
+
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
215
215
|
</p>
|
|
216
216
|
</header>
|
|
217
217
|
<div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
|
@@ -235,7 +235,7 @@ export function CachePanel() {
|
|
|
235
235
|
<div className="space-y-1">
|
|
236
236
|
<h2 className="text-lg font-semibold">{t('configs.cache.title', 'Cache overview')}</h2>
|
|
237
237
|
<p className="text-sm text-muted-foreground">
|
|
238
|
-
{t('configs.cache.description', 'Inspect cached
|
|
238
|
+
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
239
239
|
</p>
|
|
240
240
|
{stats ? (
|
|
241
241
|
<>
|
|
@@ -342,7 +342,7 @@ export function CachePanel() {
|
|
|
342
342
|
</div>
|
|
343
343
|
) : (
|
|
344
344
|
<p className="text-sm text-muted-foreground">
|
|
345
|
-
{t('configs.cache.empty', 'No cached
|
|
345
|
+
{t('configs.cache.empty', 'No cached responses for this tenant.')}
|
|
346
346
|
</p>
|
|
347
347
|
)}
|
|
348
348
|
</div>
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"configs.systemStatus.actions.purgeCacheUnavailable": "Cache service is unavailable.",
|
|
36
36
|
"configs.config.nav.cache": "Cache",
|
|
37
37
|
"configs.cache.title": "Cache overview",
|
|
38
|
-
"configs.cache.description": "Inspect cached
|
|
38
|
+
"configs.cache.description": "Inspect cached responses and clear segments when necessary.",
|
|
39
39
|
"configs.cache.loading": "Loading cache statistics…",
|
|
40
40
|
"configs.cache.loadError": "Failed to load cache statistics.",
|
|
41
41
|
"configs.cache.retry": "Retry",
|
|
42
42
|
"configs.cache.refresh": "Refresh",
|
|
43
43
|
"configs.cache.generatedAt": "Stats generated {{timestamp}}",
|
|
44
44
|
"configs.cache.totalEntries": "{{count}} cached entries",
|
|
45
|
-
"configs.cache.empty": "No cached
|
|
45
|
+
"configs.cache.empty": "No cached responses for this tenant.",
|
|
46
46
|
"configs.cache.purgeAll": "Purge all cache",
|
|
47
47
|
"configs.cache.purgeAllLoading": "Purging…",
|
|
48
48
|
"configs.cache.purgeAllConfirm": "Purge all cached entries for this tenant?",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"configs.systemStatus.categories.profilingDescription": "Flags that control request and query profiling outputs.",
|
|
65
65
|
"configs.systemStatus.categories.logging": "Logging",
|
|
66
66
|
"configs.systemStatus.categories.loggingDescription": "Tune verbosity and SQL logging for diagnostics.",
|
|
67
|
+
"configs.systemStatus.categories.security": "Security",
|
|
68
|
+
"configs.systemStatus.categories.securityDescription": "Password policy requirements enforced at login and user creation.",
|
|
67
69
|
"configs.systemStatus.categories.caching": "Caching",
|
|
68
70
|
"configs.systemStatus.categories.cachingDescription": "Cache providers and TTL controls for backend responses.",
|
|
69
71
|
"configs.systemStatus.categories.queryIndex": "Query index",
|
|
@@ -84,6 +86,14 @@
|
|
|
84
86
|
"configs.systemStatus.variables.logVerbosity.description": "Overrides structured log verbosity such as debug or trace.",
|
|
85
87
|
"configs.systemStatus.variables.logLevel.label": "Log level",
|
|
86
88
|
"configs.systemStatus.variables.logLevel.description": "Fallback log level applied when verbosity is not set.",
|
|
89
|
+
"configs.systemStatus.variables.passwordMinLength.label": "Password min length",
|
|
90
|
+
"configs.systemStatus.variables.passwordMinLength.description": "Minimum number of characters required for passwords.",
|
|
91
|
+
"configs.systemStatus.variables.passwordRequireDigit.label": "Password requires digit",
|
|
92
|
+
"configs.systemStatus.variables.passwordRequireDigit.description": "Require at least one numeric character.",
|
|
93
|
+
"configs.systemStatus.variables.passwordRequireUppercase.label": "Password requires uppercase",
|
|
94
|
+
"configs.systemStatus.variables.passwordRequireUppercase.description": "Require at least one uppercase letter.",
|
|
95
|
+
"configs.systemStatus.variables.passwordRequireSpecial.label": "Password requires special",
|
|
96
|
+
"configs.systemStatus.variables.passwordRequireSpecial.description": "Require at least one special character.",
|
|
87
97
|
"configs.systemStatus.variables.enableCrudApiCache.label": "CRUD API cache",
|
|
88
98
|
"configs.systemStatus.variables.enableCrudApiCache.description": "Enable the CRUD API response cache layer.",
|
|
89
99
|
"configs.systemStatus.variables.cacheStrategy.label": "Cache strategy",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"configs.systemStatus.actions.purgeCacheUnavailable": "Usługa pamięci podręcznej jest niedostępna.",
|
|
36
36
|
"configs.config.nav.cache": "Pamięć podręczna",
|
|
37
37
|
"configs.cache.title": "Podgląd pamięci podręcznej",
|
|
38
|
-
"configs.cache.description": "Przeglądaj zapisane odpowiedzi
|
|
38
|
+
"configs.cache.description": "Przeglądaj zapisane odpowiedzi i czyść segmenty w razie potrzeby.",
|
|
39
39
|
"configs.cache.loading": "Ładowanie statystyk pamięci podręcznej…",
|
|
40
40
|
"configs.cache.loadError": "Nie udało się wczytać statystyk pamięci podręcznej.",
|
|
41
41
|
"configs.cache.retry": "Spróbuj ponownie",
|
|
42
42
|
"configs.cache.refresh": "Odśwież",
|
|
43
43
|
"configs.cache.generatedAt": "Statystyki z {{timestamp}}",
|
|
44
44
|
"configs.cache.totalEntries": "{{count}} wpisów w pamięci podręcznej",
|
|
45
|
-
"configs.cache.empty": "Brak zapisanych odpowiedzi
|
|
45
|
+
"configs.cache.empty": "Brak zapisanych odpowiedzi dla tego tenanta.",
|
|
46
46
|
"configs.cache.purgeAll": "Wyczyść całą pamięć",
|
|
47
47
|
"configs.cache.purgeAllLoading": "Czyszczenie…",
|
|
48
48
|
"configs.cache.purgeAllConfirm": "Wyczyścić wszystkie wpisy pamięci podręcznej dla tego tenanta?",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"configs.systemStatus.categories.profilingDescription": "Flagi sterujące generowaniem danych z profilowania zapytań i żądań.",
|
|
65
65
|
"configs.systemStatus.categories.logging": "Logowanie",
|
|
66
66
|
"configs.systemStatus.categories.loggingDescription": "Dostosuj poziom logowania i zapisywanie zapytań SQL na potrzeby diagnostyki.",
|
|
67
|
+
"configs.systemStatus.categories.security": "Bezpieczeństwo",
|
|
68
|
+
"configs.systemStatus.categories.securityDescription": "Wymagania polityki haseł stosowane przy logowaniu i tworzeniu użytkowników.",
|
|
67
69
|
"configs.systemStatus.categories.caching": "Buforowanie",
|
|
68
70
|
"configs.systemStatus.categories.cachingDescription": "Mechanizmy cache oraz kontrola czasu życia odpowiedzi backendu.",
|
|
69
71
|
"configs.systemStatus.categories.queryIndex": "Indeks zapytań",
|
|
@@ -84,6 +86,14 @@
|
|
|
84
86
|
"configs.systemStatus.variables.logVerbosity.description": "Nadpisuje poziom szczegółowości logów, np. debug lub trace.",
|
|
85
87
|
"configs.systemStatus.variables.logLevel.label": "Poziom logowania",
|
|
86
88
|
"configs.systemStatus.variables.logLevel.description": "Domyślny poziom logowania używany, gdy nie ustawiono szczegółowości.",
|
|
89
|
+
"configs.systemStatus.variables.passwordMinLength.label": "Minimalna długość hasła",
|
|
90
|
+
"configs.systemStatus.variables.passwordMinLength.description": "Minimalna liczba znaków wymagana w haśle.",
|
|
91
|
+
"configs.systemStatus.variables.passwordRequireDigit.label": "Hasło wymaga cyfry",
|
|
92
|
+
"configs.systemStatus.variables.passwordRequireDigit.description": "Wymagaj co najmniej jednej cyfry.",
|
|
93
|
+
"configs.systemStatus.variables.passwordRequireUppercase.label": "Hasło wymaga wielkiej litery",
|
|
94
|
+
"configs.systemStatus.variables.passwordRequireUppercase.description": "Wymagaj co najmniej jednej wielkiej litery.",
|
|
95
|
+
"configs.systemStatus.variables.passwordRequireSpecial.label": "Hasło wymaga znaku specjalnego",
|
|
96
|
+
"configs.systemStatus.variables.passwordRequireSpecial.description": "Wymagaj co najmniej jednego znaku specjalnego.",
|
|
87
97
|
"configs.systemStatus.variables.enableCrudApiCache.label": "Cache API CRUD",
|
|
88
98
|
"configs.systemStatus.variables.enableCrudApiCache.description": "Włącza warstwę buforowania odpowiedzi API CRUD.",
|
|
89
99
|
"configs.systemStatus.variables.cacheStrategy.label": "Strategia cache",
|
|
@@ -19,7 +19,14 @@ type SystemStatusVariableDefinition = {
|
|
|
19
19
|
defaultValue: string | null
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const CATEGORY_ORDER: SystemStatusCategoryKey[] = [
|
|
22
|
+
const CATEGORY_ORDER: SystemStatusCategoryKey[] = [
|
|
23
|
+
'profiling',
|
|
24
|
+
'logging',
|
|
25
|
+
'security',
|
|
26
|
+
'caching',
|
|
27
|
+
'query_index',
|
|
28
|
+
'entities',
|
|
29
|
+
]
|
|
23
30
|
|
|
24
31
|
const CATEGORY_METADATA: Record<
|
|
25
32
|
SystemStatusCategoryKey,
|
|
@@ -33,6 +40,10 @@ const CATEGORY_METADATA: Record<
|
|
|
33
40
|
labelKey: 'configs.systemStatus.categories.logging',
|
|
34
41
|
descriptionKey: 'configs.systemStatus.categories.loggingDescription',
|
|
35
42
|
},
|
|
43
|
+
security: {
|
|
44
|
+
labelKey: 'configs.systemStatus.categories.security',
|
|
45
|
+
descriptionKey: 'configs.systemStatus.categories.securityDescription',
|
|
46
|
+
},
|
|
36
47
|
caching: {
|
|
37
48
|
labelKey: 'configs.systemStatus.categories.caching',
|
|
38
49
|
descriptionKey: 'configs.systemStatus.categories.cachingDescription',
|
|
@@ -113,6 +124,42 @@ export const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [
|
|
|
113
124
|
docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,
|
|
114
125
|
defaultValue: '',
|
|
115
126
|
},
|
|
127
|
+
{
|
|
128
|
+
key: 'OM_PASSWORD_MIN_LENGTH',
|
|
129
|
+
category: 'security',
|
|
130
|
+
kind: 'string',
|
|
131
|
+
labelKey: 'configs.systemStatus.variables.passwordMinLength.label',
|
|
132
|
+
descriptionKey: 'configs.systemStatus.variables.passwordMinLength.description',
|
|
133
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_min_length`,
|
|
134
|
+
defaultValue: '6',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: 'OM_PASSWORD_REQUIRE_DIGIT',
|
|
138
|
+
category: 'security',
|
|
139
|
+
kind: 'boolean',
|
|
140
|
+
labelKey: 'configs.systemStatus.variables.passwordRequireDigit.label',
|
|
141
|
+
descriptionKey: 'configs.systemStatus.variables.passwordRequireDigit.description',
|
|
142
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_digit`,
|
|
143
|
+
defaultValue: 'true',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: 'OM_PASSWORD_REQUIRE_UPPERCASE',
|
|
147
|
+
category: 'security',
|
|
148
|
+
kind: 'boolean',
|
|
149
|
+
labelKey: 'configs.systemStatus.variables.passwordRequireUppercase.label',
|
|
150
|
+
descriptionKey: 'configs.systemStatus.variables.passwordRequireUppercase.description',
|
|
151
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_uppercase`,
|
|
152
|
+
defaultValue: 'true',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: 'OM_PASSWORD_REQUIRE_SPECIAL',
|
|
156
|
+
category: 'security',
|
|
157
|
+
kind: 'boolean',
|
|
158
|
+
labelKey: 'configs.systemStatus.variables.passwordRequireSpecial.label',
|
|
159
|
+
descriptionKey: 'configs.systemStatus.variables.passwordRequireSpecial.description',
|
|
160
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_special`,
|
|
161
|
+
defaultValue: 'true',
|
|
162
|
+
},
|
|
116
163
|
{
|
|
117
164
|
key: 'ENABLE_CRUD_API_CACHE',
|
|
118
165
|
category: 'caching',
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import type { CacheStrategy } from '@open-mercato/cache'
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
|
+
import { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'
|
|
4
5
|
import { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'
|
|
5
6
|
import { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'
|
|
7
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
8
|
import {
|
|
7
9
|
type DateRangePreset,
|
|
8
10
|
resolveDateRange,
|
|
@@ -284,6 +286,57 @@ export class WidgetDataService {
|
|
|
284
286
|
assertSafeIdentifier(config.idColumn, 'id column')
|
|
285
287
|
assertSafeIdentifier(config.labelColumn, 'label column')
|
|
286
288
|
|
|
289
|
+
const meta = this.resolveEntityMetadata(config.table)
|
|
290
|
+
const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null
|
|
291
|
+
const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null
|
|
292
|
+
const tenantProp = meta
|
|
293
|
+
? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))
|
|
294
|
+
: null
|
|
295
|
+
const organizationProp = meta
|
|
296
|
+
? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))
|
|
297
|
+
: null
|
|
298
|
+
const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null
|
|
299
|
+
|
|
300
|
+
if (meta && idProp && labelProp && tenantProp && entityName) {
|
|
301
|
+
const where: Record<string, unknown> = {
|
|
302
|
+
[idProp]: { $in: uniqueIds },
|
|
303
|
+
[tenantProp]: this.scope.tenantId,
|
|
304
|
+
}
|
|
305
|
+
if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {
|
|
306
|
+
where[organizationProp] = { $in: this.scope.organizationIds }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const records = await findWithDecryption(
|
|
311
|
+
this.em,
|
|
312
|
+
entityName,
|
|
313
|
+
where,
|
|
314
|
+
{ fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },
|
|
315
|
+
{ tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
const labelMap = new Map<string, string>()
|
|
319
|
+
for (const record of records as Array<Record<string, unknown>>) {
|
|
320
|
+
const id = record[idProp]
|
|
321
|
+
const label = record[labelProp]
|
|
322
|
+
if (typeof id === 'string' && label != null && label !== '') {
|
|
323
|
+
labelMap.set(id, String(label))
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (labelMap.size > 0) {
|
|
328
|
+
return data.map((item) => ({
|
|
329
|
+
...item,
|
|
330
|
+
groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)
|
|
331
|
+
? labelMap.get(item.groupKey)!
|
|
332
|
+
: undefined,
|
|
333
|
+
}))
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// fall through to SQL resolution
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
287
340
|
const clauses = [`"${config.idColumn}" = ANY(?::uuid[])`, 'tenant_id = ?']
|
|
288
341
|
const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]
|
|
289
342
|
|
|
@@ -292,26 +345,27 @@ export class WidgetDataService {
|
|
|
292
345
|
params.push(`{${this.scope.organizationIds.join(',')}}`)
|
|
293
346
|
}
|
|
294
347
|
|
|
295
|
-
const sql = `SELECT "${config.idColumn}" as id, "${config.labelColumn}" as label FROM "${config.table}" WHERE ${clauses.join(
|
|
348
|
+
const sql = `SELECT "${config.idColumn}" as id, "${config.labelColumn}" as label, tenant_id, organization_id FROM "${config.table}" WHERE ${clauses.join(
|
|
296
349
|
' AND ',
|
|
297
350
|
)}`
|
|
298
351
|
|
|
299
352
|
try {
|
|
300
353
|
const labelRows = await this.em.getConnection().execute(sql, params)
|
|
301
|
-
const meta = this.resolveEntityMetadata(config.table)
|
|
302
354
|
const entityId = this.resolveEntityId(meta)
|
|
303
355
|
const encryptionService = resolveTenantEncryptionService(this.em as any)
|
|
304
356
|
const organizationId = this.resolveOrganizationId()
|
|
357
|
+
const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null
|
|
305
358
|
|
|
306
359
|
const labelMap = new Map<string, string>()
|
|
307
|
-
for (const row of labelRows as Array<{ id: string; label: string | null }>) {
|
|
360
|
+
for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {
|
|
308
361
|
let labelValue = row.label
|
|
309
362
|
if (entityId && encryptionService?.isEnabled() && labelValue != null) {
|
|
363
|
+
const rowOrgId = row.organization_id ?? organizationId ?? null
|
|
310
364
|
const decrypted = await encryptionService.decryptEntityPayload(
|
|
311
365
|
entityId,
|
|
312
366
|
{ [config.labelColumn]: labelValue },
|
|
313
367
|
this.scope.tenantId,
|
|
314
|
-
|
|
368
|
+
rowOrgId,
|
|
315
369
|
)
|
|
316
370
|
const resolved = decrypted[config.labelColumn]
|
|
317
371
|
if (typeof resolved === 'string' || typeof resolved === 'number') {
|
|
@@ -319,6 +373,13 @@ export class WidgetDataService {
|
|
|
319
373
|
}
|
|
320
374
|
}
|
|
321
375
|
|
|
376
|
+
if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {
|
|
377
|
+
const decrypted = decryptWithAesGcm(labelValue, dek.key)
|
|
378
|
+
if (decrypted !== null) {
|
|
379
|
+
labelValue = decrypted
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
322
383
|
if (row.id && labelValue != null && labelValue !== '') {
|
|
323
384
|
labelMap.set(row.id, labelValue)
|
|
324
385
|
}
|
|
@@ -359,6 +420,18 @@ export class WidgetDataService {
|
|
|
359
420
|
return match ?? null
|
|
360
421
|
}
|
|
361
422
|
|
|
423
|
+
private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {
|
|
424
|
+
const properties = meta?.properties ? Object.values(meta.properties) : []
|
|
425
|
+
for (const prop of properties as Array<Record<string, any>>) {
|
|
426
|
+
const fieldName = prop?.fieldName
|
|
427
|
+
const fieldNames = prop?.fieldNames
|
|
428
|
+
if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null
|
|
429
|
+
if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null
|
|
430
|
+
if (prop?.name === columnName) return prop?.name ?? null
|
|
431
|
+
}
|
|
432
|
+
return null
|
|
433
|
+
}
|
|
434
|
+
|
|
362
435
|
private resolveEntityId(meta: Record<string, any> | null): string | null {
|
|
363
436
|
if (!meta) return null
|
|
364
437
|
try {
|
|
@@ -367,6 +440,11 @@ export class WidgetDataService {
|
|
|
367
440
|
return null
|
|
368
441
|
}
|
|
369
442
|
}
|
|
443
|
+
|
|
444
|
+
private isEncryptedPayload(value: string): boolean {
|
|
445
|
+
const parts = value.split(':')
|
|
446
|
+
return parts.length === 4 && parts[3] === 'v1'
|
|
447
|
+
}
|
|
370
448
|
}
|
|
371
449
|
|
|
372
450
|
export function createWidgetDataService(
|