@bluemarble/bi 0.7.0 → 0.8.1
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/README.md +77 -77
- package/dist/{DashboardDevtools-D0pzwSgn.js → DashboardDevtools-TFwjFdqn.js} +1437 -565
- package/dist/DashboardDevtools-TFwjFdqn.js.map +1 -0
- package/dist/{ModelDictionary-Cw4sa8tW.js → ModelDictionary-g5d84D0n.js} +67 -5
- package/dist/ModelDictionary-g5d84D0n.js.map +1 -0
- package/dist/cli.js +0 -0
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +48 -9
- package/dist/client/index.js +186 -45
- package/dist/client/index.js.map +1 -1
- package/dist/codegen-LoQzFAle.js.map +1 -1
- package/dist/compare-DloZhg1T.js.map +1 -1
- package/dist/{formatRules-DxA9vMvz.d.ts → formatRules-C1PW2EiL.d.ts} +74 -2
- package/dist/{layout-BS3nO6OT.js → layout-BuZhfoRI.js} +167 -15
- package/dist/layout-BuZhfoRI.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +17 -1
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +81 -81
- package/dist/DashboardDevtools-D0pzwSgn.js.map +0 -1
- package/dist/ModelDictionary-Cw4sa8tW.js.map +0 -1
- package/dist/layout-BS3nO6OT.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { s as AGGREGATION_LABEL, t as canonicalJson, u as isAggregate } from "./codegen-LoQzFAle.js";
|
|
2
|
-
import {
|
|
2
|
+
import { v as resolveChartTypes } from "./layout-BuZhfoRI.js";
|
|
3
3
|
import { Box, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
|
|
4
4
|
import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -11,7 +11,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
const LAYOUT_ITEM_ATTRIBUTE = "data-bm-dashboard-layout-item";
|
|
12
12
|
const layoutItemKey = (layoutId, item) => `${layoutId}/${item.kind}:${item.key}`;
|
|
13
13
|
function parseLayoutItemKey(value) {
|
|
14
|
-
const match = /^([^/]+)\/(chart|filter):(.+)$/.exec(value);
|
|
14
|
+
const match = /^([^/]+)\/(chart|filter|parameter):(.+)$/.exec(value);
|
|
15
15
|
return match ? {
|
|
16
16
|
layoutId: match[1],
|
|
17
17
|
item: {
|
|
@@ -283,7 +283,7 @@ function createDevtoolsStore() {
|
|
|
283
283
|
//#endregion
|
|
284
284
|
//#region src/client/DashboardProvider.tsx
|
|
285
285
|
const DashboardContext = createContext(null);
|
|
286
|
-
const DashboardDevtools = lazy(() => import("./DashboardDevtools-
|
|
286
|
+
const DashboardDevtools = lazy(() => import("./DashboardDevtools-TFwjFdqn.js"));
|
|
287
287
|
const DEVTOOLS_DEFAULT = process.env.NODE_ENV === "development";
|
|
288
288
|
const draftStorageKey = (model) => `bm-dashboard-devtools:draft:${model}`;
|
|
289
289
|
function loadDraft(model) {
|
|
@@ -307,6 +307,11 @@ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEV
|
|
|
307
307
|
...current,
|
|
308
308
|
[dimension]: values
|
|
309
309
|
})), []);
|
|
310
|
+
const [parameters, setParameters] = useState({});
|
|
311
|
+
const setParameter = useCallback((name, measure) => setParameters((current) => ({
|
|
312
|
+
...current,
|
|
313
|
+
[name]: measure
|
|
314
|
+
})), []);
|
|
310
315
|
const store = useMemo(() => devtools ? createDevtoolsStore() : null, [devtools]);
|
|
311
316
|
const layouts = useMemo(() => devtools ? createLayoutStore() : null, [devtools]);
|
|
312
317
|
const [draft, setDraftState] = useState(() => devtools && typeof window !== "undefined" ? loadDraft(model) : null);
|
|
@@ -380,6 +385,8 @@ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEV
|
|
|
380
385
|
model,
|
|
381
386
|
filters,
|
|
382
387
|
setFilter,
|
|
388
|
+
parameters,
|
|
389
|
+
setParameter,
|
|
383
390
|
fetcher: queryFetcher,
|
|
384
391
|
meta,
|
|
385
392
|
refreshKey,
|
|
@@ -390,6 +397,8 @@ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEV
|
|
|
390
397
|
model,
|
|
391
398
|
filters,
|
|
392
399
|
setFilter,
|
|
400
|
+
parameters,
|
|
401
|
+
setParameter,
|
|
393
402
|
queryFetcher,
|
|
394
403
|
meta,
|
|
395
404
|
refreshKey,
|
|
@@ -596,6 +605,59 @@ function measureFormatter(meta, measure, fallbackDecimals = 2, formats) {
|
|
|
596
605
|
function fieldLabel(meta, name, labels) {
|
|
597
606
|
return labels?.[name] || meta?.dimensions[name]?.label || meta?.measures[name]?.label || name;
|
|
598
607
|
}
|
|
608
|
+
const monthNames = new Intl.DateTimeFormat("pt-BR", {
|
|
609
|
+
month: "long",
|
|
610
|
+
timeZone: "UTC"
|
|
611
|
+
});
|
|
612
|
+
const dateFormat = new Intl.DateTimeFormat("pt-BR", {
|
|
613
|
+
dateStyle: "short",
|
|
614
|
+
timeZone: "UTC"
|
|
615
|
+
});
|
|
616
|
+
const localDate = new Intl.DateTimeFormat("pt-BR", { dateStyle: "short" });
|
|
617
|
+
const localDateTime = new Intl.DateTimeFormat("pt-BR", {
|
|
618
|
+
dateStyle: "short",
|
|
619
|
+
timeStyle: "short"
|
|
620
|
+
});
|
|
621
|
+
const looseNumber = new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 2 });
|
|
622
|
+
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
623
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
624
|
+
/** `3` → "Março"; aceita também uma data ISO (usa o mês dela). Fora de 1–12, o próprio valor. */
|
|
625
|
+
function toMonthName(value) {
|
|
626
|
+
const month = typeof value === "string" && /^\d{4}-\d{2}/.test(value) ? Number(value.slice(5, 7)) : Number(value);
|
|
627
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) return String(value);
|
|
628
|
+
return capitalize(monthNames.format(new Date(Date.UTC(2e3, month - 1, 1))));
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Data ISO → `dd/mm/aaaa`. Só a data (`2026-10-01`) não passa por fuso; com hora (o `Date` do
|
|
632
|
+
* banco chega como ISO em UTC), vira o horário local e a hora só aparece quando não é meia-noite.
|
|
633
|
+
*/
|
|
634
|
+
function toDate(value) {
|
|
635
|
+
if (typeof value !== "string") return String(value);
|
|
636
|
+
const day = ISO_DATE.exec(value);
|
|
637
|
+
if (day) return dateFormat.format(new Date(Date.UTC(+day[1], +day[2] - 1, +day[3])));
|
|
638
|
+
const date = new Date(value);
|
|
639
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(value) || Number.isNaN(date.getTime())) return value;
|
|
640
|
+
if (date.getHours() || date.getMinutes()) return localDateTime.format(date).replace(",", "");
|
|
641
|
+
return localDate.format(date);
|
|
642
|
+
}
|
|
643
|
+
/** Não numérico: o próprio valor. */
|
|
644
|
+
const numeric = (format) => (value) => value === "" || Number.isNaN(Number(value)) ? String(value) : format(Number(value));
|
|
645
|
+
/**
|
|
646
|
+
* Formatters que todo `<DashboardCharts>`/`<DashboardFilters>` já tem, pelo nome (os do app com o
|
|
647
|
+
* mesmo nome vão por cima).
|
|
648
|
+
*/
|
|
649
|
+
const BUILTIN_FORMATTERS = {
|
|
650
|
+
to_month_name: toMonthName,
|
|
651
|
+
to_date: toDate,
|
|
652
|
+
to_currency: numeric((value) => formatCurrency(value)),
|
|
653
|
+
to_number: numeric((value) => looseNumber.format(value)),
|
|
654
|
+
to_string: (value) => String(value ?? "")
|
|
655
|
+
};
|
|
656
|
+
/** Os built-in mais os do app. */
|
|
657
|
+
const withBuiltinFormatters = (formatters) => ({
|
|
658
|
+
...BUILTIN_FORMATTERS,
|
|
659
|
+
...formatters
|
|
660
|
+
});
|
|
599
661
|
//#endregion
|
|
600
662
|
//#region src/client/useModelQuery.ts
|
|
601
663
|
const INSPECT_ATTRIBUTE = "data-bm-dashboard-query";
|
|
@@ -741,6 +803,6 @@ function ModelDictionary() {
|
|
|
741
803
|
});
|
|
742
804
|
}
|
|
743
805
|
//#endregion
|
|
744
|
-
export {
|
|
806
|
+
export { useDashboard as C, parseLayoutItemKey as D, layoutItemKey as E, DashboardProvider as S, LAYOUT_ITEM_ATTRIBUTE as T, TooltipBox as _, fieldLabel as a, seriesColor as b, formatNumber as c, toDate as d, toMonthName as f, SERIES_COLORS as g, OTHER_COLOR as h, BUILTIN_FORMATTERS as i, measureFormat as l, Legend as m, INSPECT_ATTRIBUTE as n, formatCurrency as o, withBuiltinFormatters as p, useModelQuery as r, formatMeasure as s, ModelDictionary as t, measureFormatter as u, chartPlaceholder as v, errorMessage as w, useClickFilter as x, numberOf as y };
|
|
745
807
|
|
|
746
|
-
//# sourceMappingURL=ModelDictionary-
|
|
808
|
+
//# sourceMappingURL=ModelDictionary-g5d84D0n.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelDictionary-g5d84D0n.js","names":["loadDraft"],"sources":["../src/devtools/layoutStore.ts","../src/client/batchQueries.ts","../src/devtools/store.ts","../src/client/DashboardProvider.tsx","../src/client/chartKit.tsx","../src/client/format.ts","../src/client/useModelQuery.ts","../src/client/ModelDictionary.tsx"],"sourcesContent":["import { canonicalJson } from '../codegen'\r\nimport type { ChartsLayout } from '../layout'\r\n\r\n/**\r\n * Layouts montados na página (`<DashboardCharts>` / `<DashboardFilters>`) e o rascunho de cada um,\r\n * editado na aba Layout do devtools. Store externo como o de consultas: editar um gráfico\r\n * re-renderiza só os componentes daquele layout e o painel.\r\n */\r\n\r\n/** Parte do layout que um componente renderiza. */\r\nexport type LayoutPart = 'charts' | 'filters' | 'canvas'\r\n\r\nexport type LayoutEntry = {\r\n /** O layout que veio do arquivo (o que a página passou ao `<DashboardCharts>`). */\r\n registered: ChartsLayout\r\n /** Rascunho editado no devtools; `null` = página usando o registrado. */\r\n draft: ChartsLayout | null\r\n /**\r\n * Partes montadas na página e os formatters que cada uma recebeu (`formatDimension`,\r\n * `formatLabel`). Parte ausente = a página não renderiza (ex.: sem `<DashboardFilters>`).\r\n */\r\n parts: Partial<Record<LayoutPart, string[]>>\r\n}\r\n\r\n/**\r\n * Marca o elemento de cada item do layout com `<layoutId>/chart:<id>` ou\r\n * `<layoutId>/filter:<dimensão>` (destaque e inspetor do devtools).\r\n */\r\nexport const LAYOUT_ITEM_ATTRIBUTE = 'data-bm-dashboard-layout-item'\r\n\r\nexport const layoutItemKey = (layoutId: string, item: LayoutItem) =>\r\n `${layoutId}/${item.kind}:${item.key}`\r\n\r\nexport type LayoutItem = { kind: 'chart' | 'filter' | 'parameter'; key: string }\r\n\r\nexport function parseLayoutItemKey(value: string): { layoutId: string; item: LayoutItem } | null {\r\n const match = /^([^/]+)\\/(chart|filter|parameter):(.+)$/.exec(value)\r\n return match\r\n ? {\r\n layoutId: match[1] as string,\r\n item: { kind: match[2] as LayoutItem['kind'], key: match[3] as string }\r\n }\r\n : null\r\n}\r\n\r\nconst draftKey = (id: string) => `bm-dashboard-devtools:layout:${id}`\r\n\r\nconst sameLayout = (a: ChartsLayout | null, b: ChartsLayout | null) =>\r\n canonicalJson(a) === canonicalJson(b)\r\n\r\nfunction loadDraft(id: string): ChartsLayout | null {\r\n try {\r\n const saved = sessionStorage.getItem(draftKey(id))\r\n return saved ? (JSON.parse(saved) as ChartsLayout) : null\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nfunction storeDraft(id: string, draft: ChartsLayout | null) {\r\n try {\r\n if (draft) sessionStorage.setItem(draftKey(id), JSON.stringify(draft))\r\n else sessionStorage.removeItem(draftKey(id))\r\n } catch {}\r\n}\r\n\r\nexport function createLayoutStore() {\r\n const entries = new Map<string, LayoutEntry>()\r\n // Montagens por layout e parte (`<id>\\u0000<parte>`)\r\n const mounts = new Map<string, number>()\r\n let snapshot: LayoutEntry[] = []\r\n const listeners = new Set<() => void>()\r\n // Edições da aba Layout ainda não aplicadas: sobrevivem a trocar de aba (o painel desmonta)\r\n const pending = new Map<string, ChartsLayout>()\r\n\r\n const emit = () => {\r\n snapshot = [...entries.values()]\r\n for (const listener of listeners) listener()\r\n }\r\n\r\n const setDraft = (id: string, next: ChartsLayout | null) => {\r\n const entry = entries.get(id)\r\n if (!entry) return\r\n // Rascunho igual ao arquivo não é rascunho (ex.: desfez tudo à mão)\r\n const draft = next && !sameLayout(next, entry.registered) ? next : null\r\n storeDraft(id, draft)\r\n entries.set(id, { ...entry, draft })\r\n emit()\r\n }\r\n\r\n return {\r\n subscribe(listener: () => void) {\r\n listeners.add(listener)\r\n return () => listeners.delete(listener)\r\n },\r\n getSnapshot: () => snapshot,\r\n get: (id: string) => entries.get(id) ?? null,\r\n /** Rascunho do layout, mesmo antes de registrar (evita renderizar o registrado e trocar). */\r\n draftOf(layout: ChartsLayout) {\r\n const entry = entries.get(layout.id)\r\n if (entry) return entry.draft\r\n const draft = loadDraft(layout.id)\r\n return draft && !sameLayout(draft, layout) ? draft : null\r\n },\r\n\r\n /** Chamado pelo `<DashboardCharts>`/`<DashboardFilters>` ao montar e quando o arquivo muda (HMR). */\r\n register(layout: ChartsLayout, part: LayoutPart, formatters: string[]) {\r\n const current = entries.get(layout.id)\r\n let draft = current ? current.draft : loadDraft(layout.id)\r\n // Depois de \"Salvar no arquivo\" o Next recarrega o JSON: o rascunho virou o registrado\r\n if (draft && sameLayout(draft, layout)) {\r\n draft = null\r\n storeDraft(layout.id, null)\r\n }\r\n const parts = { ...current?.parts, [part]: formatters }\r\n entries.set(layout.id, { registered: layout, draft, parts })\r\n const mountKey = `${layout.id}\\u0000${part}`\r\n mounts.set(mountKey, (mounts.get(mountKey) ?? 0) + 1)\r\n emit()\r\n return () => {\r\n const count = (mounts.get(mountKey) ?? 1) - 1\r\n mounts.set(mountKey, count)\r\n const entry = entries.get(layout.id)\r\n if (count > 0 || !entry) return\r\n const { [part]: _, ...rest } = entry.parts\r\n // O rascunho continua no sessionStorage e volta na próxima montagem\r\n if (Object.keys(rest).length) entries.set(layout.id, { ...entry, parts: rest })\r\n else entries.delete(layout.id)\r\n emit()\r\n }\r\n },\r\n setDraft,\r\n /** Edições ainda não aplicadas do layout (só em memória). */\r\n pendingOf: (id: string) => pending.get(id) ?? null,\r\n setPending(id: string, layout: ChartsLayout | null) {\r\n if (layout) pending.set(id, layout)\r\n else pending.delete(id)\r\n }\r\n }\r\n}\r\n\r\nexport type LayoutStore = ReturnType<typeof createLayoutStore>\r\n","import type { DashboardQuery, DashboardResult } from '../types'\r\n\r\ntype Fetcher = (query: DashboardQuery) => Promise<DashboardResult>\r\n\r\ntype Pending = {\r\n query: DashboardQuery\r\n resolve: (result: DashboardResult) => void\r\n reject: (error: unknown) => void\r\n}\r\n\r\n/**\r\n * Manda num request só (`{ model, queries }`) as consultas de um modelo feitas no mesmo ciclo — as\r\n * de um render da página. O servidor junta numa SQL só as que dão para juntar (mesmas dimensões,\r\n * filtros e fatos; ver `runBatch`): os KPIs viram uma consulta com todas as medidas, e \"Vol. Total\"\r\n * (fórmula de \"Vol. Convencional\" + \"Vol. Esteira\") reaproveita as agregações dos outros dois.\r\n *\r\n * Consultas do devtools (`query.devtools`) e de metadados passam direto. Se o lote falhar (ou o\r\n * servidor for de uma versão sem lote), cada consulta é refeita sozinha.\r\n */\r\nexport function batchQueries(fetcher: Fetcher): Fetcher {\r\n const batches = new Map<string, Pending[]>()\r\n\r\n const alone = ({ query, resolve, reject }: Pending) => fetcher(query).then(resolve, reject)\r\n\r\n const flush = (model: string) => {\r\n const members = batches.get(model) ?? []\r\n batches.delete(model)\r\n if (members.length === 1) return alone(members[0])\r\n const queries = members.map(({ query: { dimensions, measures, filters } }) => ({\r\n dimensions,\r\n measures,\r\n filters\r\n }))\r\n fetcher({ model, queries }).then(\r\n ({ results }) => {\r\n if (results?.length !== members.length) return members.forEach(alone)\r\n members.forEach(({ resolve, reject }, index) => {\r\n const { error, ...result } = results[index]\r\n if (error) reject(new Error(error))\r\n else resolve(result)\r\n })\r\n },\r\n () => members.forEach(alone)\r\n )\r\n }\r\n\r\n return (query) => {\r\n if (query.meta || query.devtools || query.queries) return fetcher(query)\r\n return new Promise((resolve, reject) => {\r\n const members = batches.get(query.model)\r\n if (members) return void members.push({ query, resolve, reject })\r\n batches.set(query.model, [{ query, resolve, reject }])\r\n // As consultas de uma renderização disparam todas nos efeitos do mesmo commit\r\n setTimeout(() => flush(query.model), 0)\r\n })\r\n }\r\n}\r\n","import type { DashboardQuery, DashboardQueryDebug, DashboardRow } from '../types'\r\n\r\n/**\r\n * Registro das consultas feitas pelos componentes (`useModelQuery`), lido pelo painel do devtools.\r\n * É um store externo (`useSyncExternalStore`) para que registrar uma consulta não re-renderize a\r\n * página inteira — só o painel.\r\n */\r\n\r\nexport type RunStatus = 'loading' | 'success' | 'error' | 'cancelled'\r\n\r\nexport type QueryRun = {\r\n runId: number\r\n instanceId: string\r\n label: string\r\n query: DashboardQuery\r\n status: RunStatus\r\n startedAt: number\r\n /** Ida e volta medida no browser. */\r\n durationMs?: number\r\n rows?: DashboardRow[]\r\n error?: string\r\n debug?: DashboardQueryDebug\r\n /** Foi num request junto com outras consultas (quantas, contando ela); o `debug` é do conjunto. */\r\n batched?: number\r\n}\r\n\r\nexport type TrackedInstance = {\r\n instanceId: string\r\n label: string\r\n runs: number\r\n last: QueryRun\r\n /** Refeita muitas vezes em pouco tempo: sinal de re-render em loop (ex.: array novo a cada render). */\r\n storm: boolean\r\n}\r\n\r\nexport type DevtoolsSnapshot = {\r\n instances: TrackedInstance[]\r\n /** Execuções mais recentes primeiro. */\r\n history: QueryRun[]\r\n}\r\n\r\nconst MAX_HISTORY = 100\r\n/** `STORM_RUNS` execuções da mesma instância dentro de `STORM_WINDOW_MS` = consulta em loop. */\r\nconst STORM_RUNS = 6\r\nconst STORM_WINDOW_MS = 5000\r\n\r\nexport function errorMessage(error: unknown): string {\r\n const err = error as {\r\n response?: { data?: { error?: string; message?: string } }\r\n message?: string\r\n }\r\n return err?.response?.data?.error ?? err?.response?.data?.message ?? err?.message ?? String(error)\r\n}\r\n\r\nexport function createDevtoolsStore() {\r\n const instances = new Map<string, TrackedInstance>()\r\n let history: QueryRun[] = []\r\n // Execuções em andamento (o histórico é limitado e pode já ter descartado uma consulta lenta)\r\n const pending = new Map<number, QueryRun>()\r\n const recentStarts = new Map<string, number[]>()\r\n // Re-execuções pedidas pelo próprio devtools (refresh, aplicar rascunho) não contam como loop\r\n let expectedUntil = 0\r\n let snapshot: DevtoolsSnapshot = { instances: [], history: [] }\r\n let nextRunId = 1\r\n const listeners = new Set<() => void>()\r\n\r\n const emit = () => {\r\n snapshot = { instances: [...instances.values()], history }\r\n for (const listener of listeners) listener()\r\n }\r\n\r\n const update = (runId: number, patch: Partial<QueryRun>) => {\r\n const current = pending.get(runId)\r\n if (!current) return\r\n pending.delete(runId)\r\n const run = { ...current, ...patch, durationMs: Date.now() - current.startedAt }\r\n history = history.map((item) => (item.runId === runId ? run : item))\r\n const instance = instances.get(run.instanceId)\r\n if (instance?.last.runId === runId) instances.set(run.instanceId, { ...instance, last: run })\r\n emit()\r\n }\r\n\r\n return {\r\n subscribe(listener: () => void) {\r\n listeners.add(listener)\r\n return () => listeners.delete(listener)\r\n },\r\n getSnapshot: () => snapshot,\r\n\r\n start(instanceId: string, label: string, query: DashboardQuery) {\r\n const run: QueryRun = {\r\n runId: nextRunId++,\r\n instanceId,\r\n label,\r\n query,\r\n status: 'loading',\r\n startedAt: Date.now()\r\n }\r\n pending.set(run.runId, run)\r\n history = [run, ...history].slice(0, MAX_HISTORY)\r\n const previous = instances.get(instanceId)\r\n const expected = run.startedAt <= expectedUntil\r\n const starts = [\r\n ...(recentStarts.get(instanceId) ?? []),\r\n ...(expected ? [] : [run.startedAt])\r\n ].filter((time) => run.startedAt - time <= STORM_WINDOW_MS)\r\n recentStarts.set(instanceId, starts)\r\n const storm = previous?.storm || starts.length >= STORM_RUNS\r\n if (storm && !previous?.storm)\r\n console.warn(\r\n `[@bluemarble/bi] \"${label}\" executou ${starts.length}x em ${STORM_WINDOW_MS / 1000}s. ` +\r\n 'Confira se o componente não recria dimensions/measures/filtros a cada render.'\r\n )\r\n instances.set(instanceId, {\r\n instanceId,\r\n label,\r\n runs: (previous?.runs ?? 0) + 1,\r\n last: run,\r\n storm\r\n })\r\n emit()\r\n return run.runId\r\n },\r\n succeed: (runId: number, rows: DashboardRow[], debug?: DashboardQueryDebug, batched?: number) =>\r\n update(runId, { status: 'success', rows, debug, batched }),\r\n fail: (runId: number, error: unknown) =>\r\n update(runId, { status: 'error', error: errorMessage(error) }),\r\n /** Consulta descartada antes de responder (filtro mudou, componente desmontou). */\r\n cancel: (runId: number) => update(runId, { status: 'cancelled' }),\r\n\r\n remove(instanceId: string) {\r\n recentStarts.delete(instanceId)\r\n if (instances.delete(instanceId)) emit()\r\n },\r\n /** Avisa que todas as consultas vão refazer agora, de propósito. */\r\n expectRefetch() {\r\n expectedUntil = Date.now() + 500\r\n },\r\n /** Zera os alertas de loop (depois de corrigir o componente, sem recarregar a página). */\r\n resetStorms() {\r\n recentStarts.clear()\r\n for (const [id, instance] of instances) instances.set(id, { ...instance, storm: false })\r\n emit()\r\n },\r\n clearHistory() {\r\n history = []\r\n emit()\r\n }\r\n }\r\n}\r\n\r\nexport type DevtoolsStore = ReturnType<typeof createDevtoolsStore>\r\n","import {\r\n type ComponentType,\r\n createContext,\r\n lazy,\r\n type ReactNode,\r\n Suspense,\r\n useCallback,\r\n useContext,\r\n useEffect,\r\n useMemo,\r\n useRef,\r\n useState\r\n} from 'react'\r\nimport { createLayoutStore, type LayoutStore } from '../devtools/layoutStore'\r\nimport { batchQueries } from './batchQueries'\r\nimport { createDevtoolsStore, type DevtoolsStore } from '../devtools/store'\r\nimport { type ChartTypeSpec, type ChartTypes, resolveChartTypes } from '../layout'\r\nimport type { ModelDef, ModelMeta } from '../model'\r\nimport type { DashboardFilters, DashboardQuery, DashboardResult, FilterValue } from '../types'\r\n\r\nexport type DashboardFetcher = (query: DashboardQuery) => Promise<DashboardResult>\r\n\r\n/**\r\n * Tipo de gráfico do app: os campos do JSON (`ChartTypeSpec`) e o componente que os renderiza. O\r\n * componente recebe os campos como props (com os `default` aplicados e formatters já resolvidos) e\r\n * `queryLabel` — ver `ChartComponentProps`.\r\n */\r\n// biome-ignore lint/suspicious/noExplicitAny: cada tipo tem as próprias props, vindas do JSON\r\nexport type ChartComponentSpec = ChartTypeSpec & { component: ComponentType<any> }\r\n\r\n/** Props de um componente registrado em `charts`: os campos do tipo + `queryLabel`. */\r\nexport type ChartComponentProps<Fields extends object = Record<string, unknown>> = Fields & {\r\n /** Nome da consulta no devtools (`<título> · <tipo>`): passe ao `useModelQuery({ label })`. */\r\n queryLabel: string\r\n}\r\n\r\n/** Só existe com o devtools ligado. */\r\nexport type DashboardDevtoolsContext = {\r\n store: DevtoolsStore\r\n /** Layouts da página (`<DashboardCharts>`/`<DashboardFilters>`) e os rascunhos da aba Layout. */\r\n layouts: LayoutStore\r\n /** Consulta o modelo registrado, ignorando o rascunho (comparações, snapshots). */\r\n rawFetcher: DashboardFetcher\r\n /** Rascunho do modelo aplicado: todas as consultas da página passam a usá-lo. */\r\n draft: ModelDef | null\r\n setDraft: (draft: ModelDef | null) => void\r\n /** Refaz todas as consultas da página. */\r\n refresh: () => void\r\n defaultFilters: DashboardFilters\r\n setFilters: (filters: DashboardFilters) => void\r\n}\r\n\r\ntype DashboardContextValue = {\r\n model: string\r\n filters: DashboardFilters\r\n setFilter: (dimension: string, values: FilterValue[]) => void\r\n /** Medida escolhida de cada parâmetro de medida do layout: `{ [name]: medida }`. */\r\n parameters: Record<string, string>\r\n setParameter: (name: string, measure: string) => void\r\n fetcher: DashboardFetcher\r\n /** Dicionário do modelo (rótulos, descrições, formatos); `null` até carregar. */\r\n meta: ModelMeta | null\r\n /** Muda quando o devtools pede para refazer as consultas. */\r\n refreshKey: number\r\n /** Tipos de gráfico do layout: os da lib e os do app (`charts`). */\r\n chartTypes: ChartTypes\r\n /** Componentes dos tipos do app, por nome. */\r\n chartComponents: Record<string, ChartComponentSpec['component']>\r\n devtools: DashboardDevtoolsContext | null\r\n}\r\n\r\nconst DashboardContext = createContext<DashboardContextValue | null>(null)\r\n\r\n// Lazy: o painel não entra no bundle da página, só é baixado quando o devtools está ligado\r\nconst DashboardDevtools = lazy(() => import('../devtools/DashboardDevtools'))\r\n\r\nconst DEVTOOLS_DEFAULT = process.env.NODE_ENV === 'development'\r\n\r\nconst draftStorageKey = (model: string) => `bm-dashboard-devtools:draft:${model}`\r\n\r\nfunction loadDraft(model: string): ModelDef | null {\r\n try {\r\n const saved = sessionStorage.getItem(draftStorageKey(model))\r\n return saved ? (JSON.parse(saved) as ModelDef) : null\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\ntype DashboardProviderProps = {\r\n /** Nome do modelo no servidor (chave do registro de modelos do app). */\r\n model: string\r\n /** Como chamar o endpoint do app (ex.: `(q) => api.post('/dashboard/query', q).then((r) => r.data)`). */\r\n fetcher: DashboardFetcher\r\n defaultFilters?: DashboardFilters\r\n /**\r\n * Painel de desenvolvimento (debug das consultas, edição do modelo). Padrão: ligado só com\r\n * `NODE_ENV=development`. O endpoint também precisa de `runQuery(..., { devtools: true })`.\r\n */\r\n devtools?: boolean\r\n /**\r\n * Tipos de gráfico do app, usáveis no layout JSON (`\"type\": \"<nome>\"`) e no editor do devtools.\r\n * Declare fora do componente (referência estável). O servidor precisa dos mesmos campos em\r\n * `devtools.chartTypes` para salvar o layout.\r\n */\r\n charts?: Record<string, ChartComponentSpec>\r\n /**\r\n * Manda as consultas de um render num request só; o servidor junta numa SQL as que têm as mesmas\r\n * dimensões, filtros e fatos (ex.: os KPIs). Padrão: ligado.\r\n */\r\n batch?: boolean\r\n children: ReactNode\r\n}\r\n\r\n/**\r\n * Guarda os filtros do dashboard. Todo componente dentro dele (filtros, gráficos, tabelas) consulta o\r\n * mesmo modelo com os mesmos filtros — é o que faz um slicer filtrar a página inteira, como no BI.\r\n */\r\nexport function DashboardProvider({\r\n model,\r\n fetcher,\r\n defaultFilters = {},\r\n devtools = DEVTOOLS_DEFAULT,\r\n charts,\r\n batch = true,\r\n children\r\n}: DashboardProviderProps) {\r\n const chartTypes = useMemo(() => resolveChartTypes(charts), [charts])\r\n const chartComponents = useMemo(\r\n () =>\r\n Object.fromEntries(\r\n Object.entries(charts ?? {}).map(([name, spec]) => [name, spec.component])\r\n ),\r\n [charts]\r\n )\r\n const [filters, setFilters] = useState<DashboardFilters>(defaultFilters)\r\n const defaultsRef = useRef(defaultFilters)\r\n\r\n const setFilter = useCallback(\r\n (dimension: string, values: FilterValue[]) =>\r\n setFilters((current) => ({ ...current, [dimension]: values })),\r\n []\r\n )\r\n\r\n // Parâmetros de medida (slicers que trocam a medida de gráficos): `{ [name]: medida }`\r\n const [parameters, setParameters] = useState<Record<string, string>>({})\r\n const setParameter = useCallback(\r\n (name: string, measure: string) =>\r\n setParameters((current) => ({ ...current, [name]: measure })),\r\n []\r\n )\r\n\r\n // --- devtools ---\r\n const store = useMemo(() => (devtools ? createDevtoolsStore() : null), [devtools])\r\n const layouts = useMemo(() => (devtools ? createLayoutStore() : null), [devtools])\r\n // O rascunho aplicado sobrevive ao reload da aba (útil enquanto se edita o modelo)\r\n const [draft, setDraftState] = useState<ModelDef | null>(() =>\r\n devtools && typeof window !== 'undefined' ? loadDraft(model) : null\r\n )\r\n const [refreshKey, setRefreshKey] = useState(0)\r\n const [mounted, setMounted] = useState(false)\r\n useEffect(() => setMounted(true), [])\r\n\r\n const setDraft = useCallback(\r\n (next: ModelDef | null) => {\r\n store?.expectRefetch()\r\n setDraftState(next)\r\n try {\r\n if (next) sessionStorage.setItem(draftStorageKey(model), JSON.stringify(next))\r\n else sessionStorage.removeItem(draftStorageKey(model))\r\n } catch {}\r\n },\r\n [model, store]\r\n )\r\n\r\n const refresh = useCallback(() => {\r\n store?.expectRefetch()\r\n setRefreshKey((key) => key + 1)\r\n }, [store])\r\n\r\n // Com o devtools ligado, toda consulta pede debug e, se houver, roda contra o rascunho\r\n const effectiveFetcher = useMemo<DashboardFetcher>(() => {\r\n if (!devtools) return fetcher\r\n return (query) =>\r\n fetcher({\r\n ...query,\r\n devtools: { debug: true, ...(draft && { model: draft }), ...query.devtools }\r\n })\r\n }, [fetcher, devtools, draft])\r\n const queryFetcher = useMemo(\r\n () => (batch ? batchQueries(effectiveFetcher) : effectiveFetcher),\r\n [batch, effectiveFetcher]\r\n )\r\n\r\n const rawFetcher = useMemo<DashboardFetcher>(\r\n () => (query) => fetcher({ ...query, devtools: { debug: true, ...query.devtools } }),\r\n [fetcher]\r\n )\r\n\r\n // Dicionário: segue o rascunho, para os rótulos editados aparecerem na hora\r\n const [meta, setMeta] = useState<ModelMeta | null>(null)\r\n useEffect(() => {\r\n let cancelado = false\r\n effectiveFetcher({ model, meta: true })\r\n .then((result) => !cancelado && setMeta(result.meta ?? null))\r\n .catch(() => !cancelado && setMeta(null))\r\n return () => {\r\n cancelado = true\r\n }\r\n }, [effectiveFetcher, model])\r\n\r\n const devtoolsValue = useMemo(\r\n () =>\r\n store &&\r\n layouts && {\r\n store,\r\n layouts,\r\n rawFetcher,\r\n draft,\r\n setDraft,\r\n refresh,\r\n defaultFilters: defaultsRef.current,\r\n setFilters\r\n },\r\n [store, layouts, rawFetcher, draft, setDraft, refresh]\r\n )\r\n\r\n const value = useMemo(\r\n () => ({\r\n model,\r\n filters,\r\n setFilter,\r\n parameters,\r\n setParameter,\r\n fetcher: queryFetcher,\r\n meta,\r\n refreshKey,\r\n chartTypes,\r\n chartComponents,\r\n devtools: devtoolsValue\r\n }),\r\n [\r\n model,\r\n filters,\r\n setFilter,\r\n parameters,\r\n setParameter,\r\n queryFetcher,\r\n meta,\r\n refreshKey,\r\n chartTypes,\r\n chartComponents,\r\n devtoolsValue\r\n ]\r\n )\r\n\r\n return (\r\n <DashboardContext.Provider value={value}>\r\n {children}\r\n {/* Só depois de montar: o painel lê localStorage e não deve participar da hidratação */}\r\n {devtoolsValue && mounted && (\r\n <Suspense fallback={null}>\r\n <DashboardDevtools />\r\n </Suspense>\r\n )}\r\n </DashboardContext.Provider>\r\n )\r\n}\r\n\r\nexport function useDashboard() {\r\n const context = useContext(DashboardContext)\r\n if (!context) throw new Error('useDashboard precisa estar dentro de <DashboardProvider>')\r\n return context\r\n}\r\n","import { Box, CircularProgress } from '@mui/material'\r\nimport type { MouseEvent, ReactNode } from 'react'\r\nimport type { DashboardRow, FilterValue } from '../types'\r\nimport { useDashboard } from './DashboardProvider'\r\n\r\n/**\r\n * Peças comuns dos gráficos ligados ao modelo: paleta, filtro por clique, legenda e os estados de\r\n * carregando/erro/vazio.\r\n */\r\n\r\n/**\r\n * Paleta categórica, sempre nesta ordem (a série N tem sempre a mesma cor). Laranja Rocha primeiro;\r\n * a ordem passa no validador de daltonismo (CVD ΔE ≥ 9 entre vizinhas) e de visão normal (≥ 19).\r\n */\r\nexport const SERIES_COLORS = [\r\n '#F58220',\r\n '#2a78d6',\r\n '#1baf7a',\r\n '#eda100',\r\n '#e87ba4',\r\n '#008300',\r\n '#6250d6',\r\n '#e34948'\r\n] as const\r\n\r\n/** \"Outros\" (fatias agrupadas) fica neutro, fora da paleta. */\r\nexport const OTHER_COLOR = '#9e9e9e'\r\n\r\nexport const seriesColor = (index: number) => SERIES_COLORS[index % SERIES_COLORS.length] as string\r\n\r\n/** Valor numérico de uma coluna da linha (`null` quando vazio). */\r\nexport const numberOf = (row: DashboardRow, key?: string) =>\r\n key && row[key] !== null && row[key] !== undefined ? Number(row[key]) : null\r\n\r\n/**\r\n * Filtro cruzado a partir do gráfico: clicar num valor filtra o dashboard por ele (Ctrl/Cmd+clique\r\n * acumula; clicar de novo no único selecionado limpa). O gráfico deve ignorar o próprio filtro na\r\n * consulta (`ignoreFilters`) e só destacar a seleção.\r\n */\r\nexport function useClickFilter(dimension: string | undefined, enabled: boolean) {\r\n const { filters, setFilter } = useDashboard()\r\n const selected = enabled && dimension ? (filters[dimension] ?? []) : []\r\n\r\n const toggle = (value: FilterValue, event?: Pick<MouseEvent, 'ctrlKey' | 'metaKey'>) => {\r\n if (!enabled || !dimension) return\r\n if (event?.ctrlKey || event?.metaKey)\r\n setFilter(\r\n dimension,\r\n selected.includes(value) ? selected.filter((item) => item !== value) : [...selected, value]\r\n )\r\n else setFilter(dimension, selected.length === 1 && selected[0] === value ? [] : [value])\r\n }\r\n\r\n return {\r\n selected,\r\n /** Há seleção e este valor não está nela. */\r\n isDimmed: (value: FilterValue) => selected.length > 0 && !selected.includes(value),\r\n toggle,\r\n clear: () => {\r\n if (enabled && dimension && selected.length) setFilter(dimension, [])\r\n }\r\n }\r\n}\r\n\r\nexport function Legend({\r\n items,\r\n loading\r\n}: {\r\n items: { label: string; color: string }[]\r\n loading?: boolean\r\n}) {\r\n return (\r\n <Box\r\n sx={{\r\n display: 'flex',\r\n flexWrap: 'wrap',\r\n columnGap: 1.5,\r\n rowGap: 0.5,\r\n mb: 1,\r\n fontSize: 14,\r\n color: '#555',\r\n minHeight: 21\r\n }}\r\n >\r\n {items.map((item) => (\r\n <Box key={item.label} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>\r\n <Box sx={{ width: 9, height: 9, borderRadius: '50%', bgcolor: item.color }} />\r\n {item.label}\r\n </Box>\r\n ))}\r\n {loading && <CircularProgress size={14} sx={{ ml: 'auto' }} />}\r\n </Box>\r\n )\r\n}\r\n\r\n/** Erro / carregando / sem dados no lugar do gráfico; `null` quando há o que desenhar (função, não componente). */\r\nexport function chartPlaceholder({\r\n height,\r\n error,\r\n isLoading,\r\n empty\r\n}: {\r\n height: number\r\n error: unknown\r\n isLoading: boolean\r\n empty: boolean\r\n}): ReactNode {\r\n if (!error && !empty) return null\r\n return (\r\n <Box\r\n sx={{\r\n height,\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n color: error ? 'error.main' : '#999'\r\n }}\r\n >\r\n {error\r\n ? 'Erro ao carregar o gráfico'\r\n : isLoading\r\n ? 'Carregando…'\r\n : 'Sem dados para os filtros selecionados'}\r\n </Box>\r\n )\r\n}\r\n\r\n/** `caption`: o que é o título (ex.: o rótulo da dimensão), em cinza acima dele. */\r\nexport function TooltipBox({\r\n title,\r\n caption,\r\n children\r\n}: {\r\n title: string\r\n caption?: string\r\n children: ReactNode\r\n}) {\r\n return (\r\n <Box sx={{ bgcolor: '#fff', border: '1px solid #ccc', p: 1, fontSize: 13 }}>\r\n {caption && <Box sx={{ fontSize: 11, color: '#777' }}>{caption}</Box>}\r\n <Box sx={{ fontWeight: 700 }}>{title}</Box>\r\n {children}\r\n </Box>\r\n )\r\n}\r\n","import type { ChartNumberFormat } from '../layout'\r\nimport type { ModelMeta } from '../model'\r\nimport type { FilterValue } from '../types'\r\n\r\nconst formatters = new Map<string, Intl.NumberFormat>()\r\n\r\n/** `compact`: notação curta do pt-BR (1,2 mil / 3,4 mi / 5,6 bi). */\r\nexport function formatNumber(\r\n value: number | string | null | undefined,\r\n decimals = 2,\r\n compact = false\r\n) {\r\n if (value === null || value === undefined || value === '') return ''\r\n const number = Number(value)\r\n if (Number.isNaN(number)) return String(value)\r\n const key = `${decimals}:${compact}`\r\n let formatter = formatters.get(key)\r\n if (!formatter) {\r\n formatter = new Intl.NumberFormat('pt-BR', {\r\n minimumFractionDigits: decimals,\r\n maximumFractionDigits: decimals,\r\n ...(compact && { notation: 'compact' })\r\n })\r\n formatters.set(key, formatter)\r\n }\r\n return formatter.format(number)\r\n}\r\n\r\nexport const formatCurrency = (\r\n value: number | string | null | undefined,\r\n decimals = 2,\r\n compact = false\r\n) => {\r\n const text = formatNumber(value, decimals, compact)\r\n return text && `R$ ${text}`\r\n}\r\n\r\n/**\r\n * Formata pelo dicionário do modelo (`format`/`decimals` da medida) ou pelo formato do gráfico.\r\n * `fallbackDecimals` é o padrão do componente quando nenhum dos dois define `decimals`.\r\n */\r\nexport function formatMeasure(\r\n value: number | string | null | undefined,\r\n meta: ChartNumberFormat | undefined,\r\n fallbackDecimals = 2\r\n) {\r\n if (value === null || value === undefined || value === '') return ''\r\n const decimals = meta?.decimals ?? (meta?.format === 'integer' ? 0 : fallbackDecimals)\r\n const compact = meta?.compact ?? false\r\n let text: string\r\n switch (meta?.format) {\r\n case 'currency':\r\n text = formatCurrency(value, decimals, compact)\r\n break\r\n case 'percent': {\r\n const number = formatNumber(Number(value) * 100, decimals, compact)\r\n text = number && `${number}%`\r\n break\r\n }\r\n default:\r\n text = formatNumber(value, decimals, compact)\r\n }\r\n return text && `${meta?.prefix ?? ''}${text}${meta?.suffix ?? ''}`\r\n}\r\n\r\n/** Dicionário do modelo com o formato do gráfico por cima. */\r\nexport function measureFormat(\r\n meta: ModelMeta | null,\r\n measure: string,\r\n formats?: Record<string, ChartNumberFormat>\r\n): ChartNumberFormat | undefined {\r\n const fromModel = meta?.measures[measure]\r\n const fromChart = formats?.[measure]\r\n if (!fromChart) return fromModel\r\n return {\r\n ...(fromModel?.format !== undefined && { format: fromModel.format }),\r\n ...(fromModel?.decimals !== undefined && { decimals: fromModel.decimals }),\r\n ...fromChart\r\n }\r\n}\r\n\r\n/**\r\n * Formatter de uma medida: o `formats` do gráfico (se houver) por cima do dicionário do modelo; sem\r\n * `format`, número com `fallbackDecimals`.\r\n */\r\nexport function measureFormatter(\r\n meta: ModelMeta | null,\r\n measure: string,\r\n fallbackDecimals = 2,\r\n formats?: Record<string, ChartNumberFormat>\r\n): (value: number | string | null | undefined) => string {\r\n const format = measureFormat(meta, measure, formats)\r\n return (value) => formatMeasure(value, format, fallbackDecimals)\r\n}\r\n\r\n/** Rótulo de uma dimensão/medida: o `labels` do gráfico, o dicionário do modelo ou o nome. */\r\nexport function fieldLabel(\r\n meta: ModelMeta | null,\r\n name: string,\r\n labels?: Record<string, string>\r\n): string {\r\n return labels?.[name] || meta?.dimensions[name]?.label || meta?.measures[name]?.label || name\r\n}\r\n\r\nconst monthNames = new Intl.DateTimeFormat('pt-BR', { month: 'long', timeZone: 'UTC' })\r\nconst dateFormat = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeZone: 'UTC' })\r\nconst localDate = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short' })\r\nconst localDateTime = new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' })\r\nconst looseNumber = new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 })\r\n\r\nconst capitalize = (text: string) => text.charAt(0).toUpperCase() + text.slice(1)\r\nconst ISO_DATE = /^(\\d{4})-(\\d{2})-(\\d{2})$/\r\n\r\n/** `3` → \"Março\"; aceita também uma data ISO (usa o mês dela). Fora de 1–12, o próprio valor. */\r\nexport function toMonthName(value: FilterValue): string {\r\n const iso = typeof value === 'string' && /^\\d{4}-\\d{2}/.test(value)\r\n const month = iso ? Number(value.slice(5, 7)) : Number(value)\r\n if (!Number.isInteger(month) || month < 1 || month > 12) return String(value)\r\n return capitalize(monthNames.format(new Date(Date.UTC(2000, month - 1, 1))))\r\n}\r\n\r\n/**\r\n * Data ISO → `dd/mm/aaaa`. Só a data (`2026-10-01`) não passa por fuso; com hora (o `Date` do\r\n * banco chega como ISO em UTC), vira o horário local e a hora só aparece quando não é meia-noite.\r\n */\r\nexport function toDate(value: FilterValue): string {\r\n if (typeof value !== 'string') return String(value)\r\n const day = ISO_DATE.exec(value)\r\n if (day) return dateFormat.format(new Date(Date.UTC(+day[1], +day[2] - 1, +day[3])))\r\n const date = new Date(value)\r\n if (!/^\\d{4}-\\d{2}-\\d{2}T/.test(value) || Number.isNaN(date.getTime())) return value\r\n if (date.getHours() || date.getMinutes()) return localDateTime.format(date).replace(',', '')\r\n return localDate.format(date)\r\n}\r\n\r\n/** Não numérico: o próprio valor. */\r\nconst numeric = (format: (value: number) => string) => (value: FilterValue) =>\r\n value === '' || Number.isNaN(Number(value)) ? String(value) : format(Number(value))\r\n\r\n/**\r\n * Formatters que todo `<DashboardCharts>`/`<DashboardFilters>` já tem, pelo nome (os do app com o\r\n * mesmo nome vão por cima).\r\n */\r\nexport const BUILTIN_FORMATTERS: Record<string, (value: FilterValue) => string> = {\r\n to_month_name: toMonthName,\r\n to_date: toDate,\r\n to_currency: numeric((value) => formatCurrency(value)),\r\n to_number: numeric((value) => looseNumber.format(value)),\r\n to_string: (value) => String(value ?? '')\r\n}\r\n\r\n/** Os built-in mais os do app. */\r\nexport const withBuiltinFormatters = (\r\n formatters: Record<string, (value: FilterValue) => string>\r\n) => ({ ...BUILTIN_FORMATTERS, ...formatters })\r\n","import { useEffect, useId, useMemo, useState } from 'react'\r\nimport type { DashboardFilters, DashboardRow } from '../types'\r\nimport { useDashboard } from './DashboardProvider'\r\n\r\nexport const INSPECT_ATTRIBUTE = 'data-bm-dashboard-query'\r\n\r\ntype UseModelQueryParams = {\r\n dimensions?: string[]\r\n measures?: string[]\r\n /**\r\n * Filtros do dashboard que esta consulta NÃO deve respeitar. Um filtro ignora a si mesmo para\r\n * listar as opções (senão, ao escolher um navio, a lista mostraria só ele).\r\n */\r\n ignoreFilters?: string[]\r\n /** Nome da consulta no devtools (padrão: dimensões × medidas). */\r\n label?: string\r\n /** Não consulta (devolve `rows: []`); ex.: a linha de total de uma tabela que não mostra total. */\r\n skip?: boolean\r\n}\r\n\r\nexport function useModelQuery({\r\n dimensions,\r\n measures,\r\n ignoreFilters,\r\n label,\r\n skip = false\r\n}: UseModelQueryParams) {\r\n const { model, filters, fetcher, refreshKey, devtools } = useDashboard()\r\n const instanceId = useId()\r\n\r\n const applied: DashboardFilters = {}\r\n for (const [dimension, values] of Object.entries(filters)) {\r\n if (values.length && !ignoreFilters?.includes(dimension)) applied[dimension] = values\r\n }\r\n\r\n // Chave por valor: só refaz a consulta quando o que vai pro servidor muda (não a cada render,\r\n // nem quando muda um filtro que esta consulta ignora)\r\n const key = JSON.stringify([model, dimensions, measures, applied])\r\n const query = useMemo(() => ({ model, dimensions, measures, filters: applied }), [key])\r\n\r\n const [rows, setRows] = useState<DashboardRow[]>([])\r\n const [isLoading, setIsLoading] = useState(true)\r\n const [error, setError] = useState<unknown>()\r\n\r\n const store = devtools?.store\r\n const debugLabel =\r\n label ?? [dimensions?.join(', '), measures?.join(', ')].filter(Boolean).join(' × ')\r\n\r\n useEffect(() => {\r\n if (skip) {\r\n setRows([])\r\n setIsLoading(false)\r\n return\r\n }\r\n let cancelado = false\r\n setIsLoading(true)\r\n const runId = store?.start(instanceId, debugLabel, query)\r\n fetcher(query)\r\n .then((result) => {\r\n if (cancelado) return\r\n setRows(result.rows)\r\n setError(undefined)\r\n if (runId) store?.succeed(runId, result.rows, result.devtools?.debug, result.batched)\r\n })\r\n .catch((err) => {\r\n if (cancelado) return\r\n setError(err)\r\n if (runId) store?.fail(runId, err)\r\n })\r\n .finally(() => !cancelado && setIsLoading(false))\r\n return () => {\r\n cancelado = true\r\n if (runId) store?.cancel(runId)\r\n }\r\n }, [fetcher, query, refreshKey, skip])\r\n\r\n useEffect(() => (store ? () => store.remove(instanceId) : undefined), [store, instanceId, skip])\r\n\r\n // Espalhar no elemento raiz do componente: é como o inspetor do devtools acha o componente na tela\r\n const inspectProps = store && !skip ? { [INSPECT_ATTRIBUTE]: instanceId } : {}\r\n\r\n return { rows, isLoading, error, inspectProps }\r\n}\r\n","import {\r\n Box,\r\n Table,\r\n TableBody,\r\n TableCell,\r\n TableContainer,\r\n TableHead,\r\n TableRow,\r\n Typography\r\n} from '@mui/material'\r\nimport { AGGREGATION_LABEL, isAggregate, type ModelMeta, type ValueFormat } from '../model'\r\nimport { useDashboard } from './DashboardProvider'\r\n\r\nconst FORMAT_LABEL: Record<ValueFormat, string> = {\r\n number: 'Número',\r\n integer: 'Inteiro',\r\n currency: 'Moeda (R$)',\r\n percent: 'Percentual'\r\n}\r\n\r\n/** Como a medida é calculada, em termos de negócio (o SQL não sai do servidor). */\r\nfunction formula(meta: ModelMeta, name: string) {\r\n const measure = meta.measures[name]\r\n const label = (ref: string) => meta.measures[ref]?.label ?? ref\r\n if (measure.type === 'formula' && measure.refs?.length)\r\n return `Combina ${measure.refs.map(label).join(', ')}`\r\n if (isAggregate(measure)) return AGGREGATION_LABEL[measure.type]\r\n if (measure.type !== 'ratio' || !measure.numerator || !measure.denominator) return '—'\r\n return `${label(measure.numerator)} ÷ ${label(measure.denominator)}`\r\n}\r\n\r\n/**\r\n * Dicionário do modelo do `DashboardProvider`: o que cada filtro/eixo e cada número significa. É\r\n * montado a partir de `label`/`description` das dimensões e medidas — documentação que não\r\n * desatualiza, porque é a mesma definição que calcula os números.\r\n */\r\nexport function ModelDictionary() {\r\n const { meta } = useDashboard()\r\n if (!meta) return null\r\n\r\n const header = (labels: string[]) => (\r\n <TableHead>\r\n <TableRow>\r\n {labels.map((label) => (\r\n <TableCell key={label} sx={{ fontWeight: 700 }}>\r\n {label}\r\n </TableCell>\r\n ))}\r\n </TableRow>\r\n </TableHead>\r\n )\r\n\r\n return (\r\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\r\n <Box>\r\n <Typography sx={{ fontWeight: 700, mb: 1 }}>Indicadores</Typography>\r\n <TableContainer>\r\n <Table size=\"small\">\r\n {header(['Indicador', 'Descrição', 'Cálculo', 'Formato'])}\r\n <TableBody>\r\n {Object.entries(meta.measures).map(([name, measure]) => (\r\n <TableRow key={name}>\r\n <TableCell sx={{ whiteSpace: 'nowrap' }}>{measure.label ?? name}</TableCell>\r\n <TableCell>{measure.description ?? '—'}</TableCell>\r\n <TableCell sx={{ whiteSpace: 'nowrap' }}>{formula(meta, name)}</TableCell>\r\n <TableCell sx={{ whiteSpace: 'nowrap' }}>\r\n {measure.format ? FORMAT_LABEL[measure.format] : '—'}\r\n </TableCell>\r\n </TableRow>\r\n ))}\r\n </TableBody>\r\n </Table>\r\n </TableContainer>\r\n </Box>\r\n <Box>\r\n <Typography sx={{ fontWeight: 700, mb: 1 }}>Filtros e agrupamentos</Typography>\r\n <TableContainer>\r\n <Table size=\"small\">\r\n {header(['Campo', 'Descrição'])}\r\n <TableBody>\r\n {Object.entries(meta.dimensions).map(([name, dimension]) => (\r\n <TableRow key={name}>\r\n <TableCell sx={{ whiteSpace: 'nowrap' }}>{dimension.label ?? name}</TableCell>\r\n <TableCell>{dimension.description ?? '—'}</TableCell>\r\n </TableRow>\r\n ))}\r\n </TableBody>\r\n </Table>\r\n </TableContainer>\r\n </Box>\r\n </Box>\r\n )\r\n}\r\n"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\n * quem grava é o CLI (`bm-bi new`).\n */\n\nexport class ScaffoldError extends Error {}\n\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\nconst LAYOUT_ID = /^[\\w-]+$/\n\nexport function checkModelName(name: string) {\n if (!IDENTIFIER.test(name))\n throw new ScaffoldError(\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\n )\n}\n\nexport function checkLayoutId(id: string) {\n if (!LAYOUT_ID.test(id))\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\n}\n\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\nexport function modelFileCode(name: string) {\n checkModelName(name)\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\n 'export const',\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\n )\n}\n\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\nexport function layoutFileCode(id: string) {\n checkLayoutId(id)\n const layout: ChartsLayout = { id, filters: [], charts: [] }\n return `${JSON.stringify(layout, null, 2)}\\n`\n}\n\n/** `models/index.ts` novo, com só este modelo. */\nexport function modelIndexCode(name: string) {\n checkModelName(name)\n return [\n `import { ${name} } from './${name}'`,\n '',\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\n 'export const dashboardModels = {',\n ` ${name}`,\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\n */\nexport function registerModel(source: string, name: string): string | null {\n checkModelName(name)\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\n if (importPath.test(source)) return null\n\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\n if (!object)\n throw new ScaffoldError(\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\n )\n const open = object.index + object[0].length - 1\n let depth = 0\n let close = -1\n for (let i = open; i < source.length; i++) {\n if (source[i] === '{') depth++\n else if (source[i] === '}' && --depth === 0) {\n close = i\n break\n }\n }\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\n\n const body = source.slice(open + 1, close)\n const items = body.trimEnd()\n let newBody: string\n if (!items.trim()) newBody = `\\n ${name}\\n`\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\n\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\n const line = `import { ${name} } from './${name}'\\n`\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\n const last = imports.at(-1)\n const at = last ? last.index + last[0].length : 0\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\n return result\n}\n\n// --- página ---\n\n/** `analiseCusto` → `analise-custo` (rota da página). */\nexport const kebabCase = (text: string) =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase()\n\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\nexport const titleCase = (text: string) =>\n kebabCase(text)\n .split('-')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\n\nexport type PageOptions = {\n model: string\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\n layoutImport: string\n title: string\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\n endpoint: string\n /** App Router do Next: a página vira client component (`'use client'`). */\n clientDirective?: boolean\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\n chartsImport?: string\n}\n\n/**\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\n * dashboard com esses marcadores no lugar do que muda de um para outro.\n */\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\n __MODEL__: options.model,\n __LAYOUT__: options.layoutImport,\n __TITLE__: options.title,\n __COMPONENT__: pageComponentName(options.model),\n __ENDPOINT__: options.endpoint\n})\n\nexport function fillPageTemplate(template: string, options: PageOptions) {\n let content = template\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\nexport function pageFileCode(options: PageOptions) {\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\n checkModelName(model)\n return [\n ...(clientDirective ? [\"'use client'\", ''] : []),\n 'import {',\n ' DashboardCanvas,',\n ' DashboardCharts,',\n ' type DashboardFetcher,',\n ' DashboardFilters,',\n ' DashboardProvider',\n \"} from '@bluemarble/bi/client'\",\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\n \"import { Box, Typography } from '@mui/material'\",\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\n `import layout from '${layoutImport}'`,\n '',\n '/**',\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\n ' */',\n '',\n `const TITULO = ${literal(title)}`,\n 'const chartsLayout = layout as ChartsLayout',\n '',\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\n 'const fetcher: DashboardFetcher = async (query) => {',\n ` const response = await fetch('${endpoint}', {`,\n \" method: 'POST',\",\n \" headers: { 'Content-Type': 'application/json' },\",\n ' body: JSON.stringify(query)',\n ' })',\n ' if (!response.ok) throw new Error(await response.text())',\n ' return (await response.json()) as DashboardResult',\n '}',\n '',\n `export default function ${pageComponentName(model)}() {`,\n ' return (',\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\n ' <Box sx={{ p: 2 }}>',\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\n ' <DashboardCanvas layout={chartsLayout}>',\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\n ' <Typography component=\"h1\" variant=\"h5\">',\n ' {TITULO}',\n ' </Typography>',\n ' <DashboardFilters layout={chartsLayout} />',\n ' <DashboardCharts layout={chartsLayout} />',\n ' </Box>',\n ' </DashboardCanvas>',\n ' </Box>',\n ' </DashboardProvider>',\n ' )',\n '}',\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\n * `custo`). Sem a opção (ou sem config), `tsx`.\n */\nexport function pageExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('tsx')) ?? 'tsx'\n}\n\n/** Itens do `pageExtensions` do `next.config` (lido como texto). */\nfunction pageExtensions(nextConfig: string | undefined) {\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\n return [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1] as string)\n}\n\n// --- endpoint ---\n\nexport type ApiOptions = {\n /** Import do `models/index.ts` (o `dashboardModels`), relativo ao endpoint. */\n modelsImport: string\n /** Pastas do devtools, relativas à raiz do app (`process.cwd()` no servidor). */\n modelsDir: string\n layoutsDir: string\n snapshotsDir: string\n /** Import do registro de tipos de gráfico do servidor (`export const chartTypes`). */\n chartTypesImport?: string\n /** App Router do Next: `route.ts` com `POST(request)` em vez do handler do Pages Router. */\n appRouter?: boolean\n}\n\n/** Valores trocados num template de endpoint do app (`api.template`). */\nexport const apiTemplateValues = (options: ApiOptions): Record<string, string> => ({\n __MODELS__: options.modelsImport,\n __MODELS_DIR__: options.modelsDir,\n __LAYOUTS_DIR__: options.layoutsDir,\n __SNAPSHOTS_DIR__: options.snapshotsDir\n})\n\nexport function fillApiTemplate(template: string, options: ApiOptions) {\n let content = template\n for (const [marker, value] of Object.entries(apiTemplateValues(options)))\n content = content.replaceAll(marker, value)\n return content\n}\n\n/** `src/dashboard/models` → `'src', 'dashboard', 'models'` (argumentos do `path.join`). */\nconst pathArgs = (dir: string) =>\n dir\n .split(/[\\\\/]/)\n .filter(Boolean)\n .map((part) => literal(part))\n .join(', ')\n\n/**\n * Endpoint padrão do `runQuery` com Prisma: um só para todos os dashboards (só aceita os modelos do\n * `models/index.ts`), com o devtools ligado só em desenvolvimento.\n */\nexport function apiFileCode(options: ApiOptions) {\n const { modelsImport, chartTypesImport, appRouter } = options\n const dir = (path: string) => `path.join(process.cwd(), ${pathArgs(path)})`\n const run = (body: string, indent: string) =>\n [\n 'const result = await runQuery(',\n ` dashboardModels,`,\n ` ${body} as DashboardQuery,`,\n ' (sql, params) => prisma.$queryRawUnsafe<unknown[]>(sql, ...params),',\n ' { cacheTtlMs: CACHE_TTL_MS, devtools: DEVTOOLS }',\n ')'\n ].map((line) => `${indent}${line}`)\n return [\n \"import path from 'node:path'\",\n \"import { DashboardQueryError, runQuery } from '@bluemarble/bi/server'\",\n \"import type { DashboardQuery } from '@bluemarble/bi/types'\",\n \"import { PrismaClient } from '@prisma/client'\",\n ...(appRouter ? [] : [\"import type { NextApiRequest, NextApiResponse } from 'next'\"]),\n ...(chartTypesImport ? [`import { chartTypes } from '${chartTypesImport}'`] : []),\n `import { dashboardModels } from '${modelsImport}'`,\n '',\n '// Client próprio: rotas que chamam `prisma.$disconnect()` no client global derrubariam',\n '// consultas longas em andamento (\"Engine is not yet connected\").',\n 'const globalForPrisma = globalThis as unknown as { dashboardPrisma?: PrismaClient }',\n 'const prisma = globalForPrisma.dashboardPrisma ?? new PrismaClient()',\n \"if (process.env.NODE_ENV === 'development') globalForPrisma.dashboardPrisma = prisma\",\n '',\n 'const CACHE_TTL_MS = 5 * 60 * 1000',\n '',\n '// Devtools (debug, rascunho de modelo, salvar modelo/layout) aceita SQL vindo do browser: só em dev',\n \"const DEVTOOLS = process.env.NODE_ENV === 'development' && {\",\n ` modelsDir: ${dir(options.modelsDir)},`,\n ` snapshotsDir: ${dir(options.snapshotsDir)},`,\n ` layoutsDir: ${dir(options.layoutsDir)}${chartTypesImport ? ',' : ''}`,\n ...(chartTypesImport ? [' chartTypes'] : []),\n '}',\n '',\n '/**',\n ' * Endpoint genérico do `@bluemarble/bi`: recebe `{ model, dimensions, measures, filters }` e',\n ' * devolve as linhas agregadas. Só aceita nomes definidos nos modelos registrados.',\n ' */',\n ...(appRouter\n ? [\n 'export async function POST(request: Request) {',\n ' try {',\n ...run('(await request.json())', ' '),\n ' return Response.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return Response.json({ error: error.message }, { status: 400 })',\n ' throw error',\n ' }',\n '}'\n ]\n : [\n 'export default async function handler(req: NextApiRequest, res: NextApiResponse) {',\n \" if (req.method !== 'POST') return res.status(405).setHeader('Allow', 'POST').end()\",\n ' try {',\n ...run('req.body', ' '),\n ' return res.json(result)',\n ' } catch (error) {',\n ' if (error instanceof DashboardQueryError)',\n ' return res.status(400).json({ error: error.message })',\n ' throw error',\n ' }',\n '}'\n ]),\n ''\n ].join('\\n')\n}\n\n/**\n * Extensão das rotas de API do Pages Router pelo `pageExtensions` do `next.config` (ex.: `api.ts`\n * no `custo`). Sem a opção (ou sem config), `ts`.\n */\nexport function apiExtension(nextConfig: string | undefined) {\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('ts')) ?? 'ts'\n}\n","#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename, dirname, join, relative, sep } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport {\n type ApiOptions,\n apiExtension,\n apiFileCode,\n checkLayoutId,\n checkModelName,\n fillApiTemplate,\n fillPageTemplate,\n kebabCase,\n layoutFileCode,\n modelFileCode,\n modelIndexCode,\n type PageOptions,\n pageExtension,\n pageFileCode,\n registerModel,\n ScaffoldError,\n titleCase\n} from './scaffold'\n\nconst HELP = `Uso: bm-bi new <nome> [opções]\n\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\n(layouts/<id>.json), a página e, se ainda não existir, o endpoint do runQuery (um só para todos os\ndashboards). Depois é montar tudo pelas abas Modelo e Layout do devtools.\n\nA página vai para o router do Next que existir: src/pages (ou pages) →\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\n\nO endpoint vai para a rota do --endpoint no mesmo router: src/pages → api/dashboard/query.<ext> (ext\nde API do pageExtensions); src/app → api/dashboard/query/route.ts. O padrão usa Prisma\n($queryRawUnsafe); com <dir>/api.template, sai dele, trocando __MODELS__ (import do models/index),\n__MODELS_DIR__, __LAYOUTS_DIR__ e __SNAPSHOTS_DIR__ (pastas relativas à raiz do app).\n\nOpções:\n --id <id> id do layout (padrão: o nome do modelo)\n --title <texto> título da página (padrão: o id em palavras)\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\n --skip-page não cria a página\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\n --endpoint <url> endpoint do runQuery (padrão: /api/dashboard/query)\n --api <arquivo> arquivo do endpoint (padrão: pela rota do --endpoint, ver acima)\n --skip-api não cria o endpoint\n --api-template <a.> template do endpoint (padrão: <dir>/api.template, se existir)\n --snapshots <pasta> pasta dos snapshots no endpoint (padrão: tests/dashboard/snapshots)\n -h, --help esta ajuda\n`\n\nconst exists = (path: string) => existsSync(path)\n\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\nfunction importPath(from: string, to: string) {\n const path = relative(dirname(from), to).split(sep).join('/')\n return path.startsWith('.') ? path : `./${path}`\n}\n\n/** Router do Next que existir no app (Pages primeiro, como o `custo`). */\nfunction nextRouter(cwd: string) {\n for (const pages of ['src/pages', 'pages']) {\n if (!exists(join(cwd, pages))) continue\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\n .map((file) => join(cwd, file))\n .find(exists)\n return { app: false, dir: join(cwd, pages), config: config && readFileSync(config, 'utf8') }\n }\n for (const app of ['src/app', 'app'])\n if (exists(join(cwd, app))) return { app: true, dir: join(cwd, app), config: undefined }\n return undefined\n}\n\ntype Router = ReturnType<typeof nextRouter>\n\nfunction defaultPagePath(router: Router, slug: string) {\n if (!router) return undefined\n return router.app\n ? join(router.dir, 'dashboard', slug, 'page.tsx')\n : join(router.dir, 'dashboard', slug, `index.${pageExtension(router.config)}`)\n}\n\n/** Endpoint na rota do `--endpoint` (ex.: `/api/dashboard/query`). */\nfunction defaultApiPath(router: Router, endpoint: string) {\n if (!router) return undefined\n if (!/^(\\/[\\w-]+)+$/.test(endpoint))\n throw new ScaffoldError(\n `Endpoint inválido para criar a rota: ${endpoint} (use --api ou --skip-api)`\n )\n const route = endpoint.slice(1).split('/')\n return router.app\n ? join(router.dir, ...route, 'route.ts')\n : join(router.dir, ...route.slice(0, -1), `${route.at(-1)}.${apiExtension(router.config)}`)\n}\n\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\nfunction chartsIndex(dir: string) {\n return ['index.ts', 'index.tsx']\n .map((file) => join(dir, 'charts', file))\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\n}\n\n/** `export const chartTypes` em <dir>/charts/types.ts: o devtools do endpoint valida o layout. */\nfunction chartTypesFile(dir: string) {\n return ['types.ts', 'types.tsx']\n .map((file) => join(dir, 'charts', file))\n .find(\n (path) => exists(path) && /export\\s+const\\s+chartTypes\\b/.test(readFileSync(path, 'utf8'))\n )\n}\n\n/** Caminho relativo à raiz do app, sempre com `/`. */\nconst fromRoot = (cwd: string, path: string) => relative(cwd, path).split(sep).join('/')\n\nfunction main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n id: { type: 'string' },\n dir: { type: 'string', default: 'src/dashboard' },\n models: { type: 'string' },\n layouts: { type: 'string' },\n title: { type: 'string' },\n page: { type: 'string' },\n 'skip-page': { type: 'boolean' },\n template: { type: 'string' },\n endpoint: { type: 'string', default: '/api/dashboard/query' },\n api: { type: 'string' },\n 'skip-api': { type: 'boolean' },\n 'api-template': { type: 'string' },\n snapshots: { type: 'string', default: 'tests/dashboard/snapshots' },\n help: { type: 'boolean', short: 'h' }\n }\n })\n const [command, name, ...rest] = positionals\n if (values.help || !command) {\n process.stdout.write(HELP)\n return\n }\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\n\n const id = values.id ?? name\n checkModelName(name)\n checkLayoutId(id)\n\n const cwd = process.cwd()\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\n const modelPath = join(modelsDir, `${name}.ts`)\n const indexPath = join(modelsDir, 'index.ts')\n const layoutPath = join(layoutsDir, `${id}.json`)\n const dashboardDir = join(cwd, values.dir)\n const router = nextRouter(cwd)\n const pagePath = values['skip-page']\n ? undefined\n : values.page\n ? join(cwd, values.page)\n : defaultPagePath(router, kebabCase(id))\n const apiPath = values['skip-api']\n ? undefined\n : values.api\n ? join(cwd, values.api)\n : defaultApiPath(router, values.endpoint)\n // O endpoint é um só para todos os dashboards: se já existe, fica como está\n const apiExists = !!apiPath && exists(apiPath)\n const templatePath = values.template\n ? join(cwd, values.template)\n : [join(dashboardDir, 'page.template')].find(exists)\n const apiTemplatePath = values['api-template']\n ? join(cwd, values['api-template'])\n : [join(dashboardDir, 'api.template')].find(exists)\n for (const path of [templatePath, apiTemplatePath])\n if (path && !exists(path))\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, path)}`)\n\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\n for (const path of [modelPath, layoutPath, pagePath])\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\n const index = existsSync(indexPath)\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\n : modelIndexCode(name)\n let page: string | undefined\n if (pagePath) {\n const charts = chartsIndex(dashboardDir)\n const options: PageOptions = {\n model: name,\n layoutImport: importPath(pagePath, layoutPath),\n title: values.title ?? titleCase(id),\n endpoint: values.endpoint,\n // App Router: a página usa hooks, então é client component\n clientDirective:\n basename(pagePath).startsWith('page.') &&\n relative(cwd, pagePath).split(sep).includes('app'),\n chartsImport: charts && importPath(pagePath, dirname(charts))\n }\n page = templatePath\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\n : pageFileCode(options)\n }\n let api: string | undefined\n if (apiPath && !apiExists) {\n const chartTypes = chartTypesFile(dashboardDir)\n const options: ApiOptions = {\n modelsImport: importPath(apiPath, modelsDir),\n modelsDir: fromRoot(cwd, modelsDir),\n layoutsDir: fromRoot(cwd, layoutsDir),\n snapshotsDir: fromRoot(cwd, join(cwd, values.snapshots)),\n chartTypesImport: chartTypes && importPath(apiPath, chartTypes.replace(/\\.tsx?$/, '')),\n appRouter: basename(apiPath).startsWith('route.')\n }\n api = apiTemplatePath\n ? fillApiTemplate(readFileSync(apiTemplatePath, 'utf8'), options)\n : apiFileCode(options)\n }\n\n const written: string[] = []\n const write = (path: string, content: string) => {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n written.push(relative(cwd, path))\n }\n write(modelPath, modelFileCode(name))\n if (index !== null) write(indexPath, index)\n write(layoutPath, layoutFileCode(id))\n if (pagePath && page !== undefined) write(pagePath, page)\n if (apiPath && api !== undefined) write(apiPath, api)\n\n process.stdout.write(\n [\n ...written.map((path) => `✓ ${path}`),\n ...(pagePath\n ? templatePath\n ? [` (página do template ${relative(cwd, templatePath)})`]\n : []\n : values['skip-page']\n ? []\n : [\n '',\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\n ' <DashboardFilters layout={layout} />',\n ' <DashboardCharts layout={layout} />',\n ' </DashboardProvider>'\n ]),\n ...(apiPath\n ? apiExists\n ? [`· ${relative(cwd, apiPath)} (endpoint já existe, mantido)`]\n : apiTemplatePath\n ? [` (endpoint do template ${relative(cwd, apiTemplatePath)})`]\n : []\n : values['skip-api']\n ? []\n : ['', 'Endpoint não criado: não achei src/pages nem src/app. Use --api <arquivo>.']),\n ''\n ].join('\\n')\n )\n}\n\ntry {\n main(process.argv.slice(2))\n} catch (error) {\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\n process.stderr.write(`${error.message}\\n`)\n process.exitCode = 1\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/scaffold.ts","../src/cli/index.ts"],"sourcesContent":["import { literal, toModelCode } from '../codegen'\r\nimport type { ChartsLayout } from '../layout'\r\n\r\n/**\r\n * Arquivos de um dashboard novo: o modelo vazio (`models/<nome>.ts`), o registro dele no\r\n * `models/index.ts` e o layout vazio (`layouts/<id>.json`). Tudo como texto, sem tocar no disco —\r\n * quem grava é o CLI (`bm-bi new`).\r\n */\r\n\r\nexport class ScaffoldError extends Error {}\r\n\r\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\r\nconst LAYOUT_ID = /^[\\w-]+$/\r\n\r\nexport function checkModelName(name: string) {\r\n if (!IDENTIFIER.test(name))\r\n throw new ScaffoldError(\r\n `Nome de modelo inválido: \"${name}\" (use um identificador JS, ex.: analiseCusto)`\r\n )\r\n}\r\n\r\nexport function checkLayoutId(id: string) {\r\n if (!LAYOUT_ID.test(id))\r\n throw new ScaffoldError(`Id de layout inválido: \"${id}\" (só letras, números, _ e -)`)\r\n}\r\n\r\n/** Modelo vazio, pronto para montar pelo diagrama da aba Modelo do devtools. */\r\nexport function modelFileCode(name: string) {\r\n checkModelName(name)\r\n return toModelCode({ name, tables: {}, dimensions: {}, measures: {} }).replace(\r\n 'export const',\r\n '/** Monte tabelas, relacionamentos, dimensões e medidas pela aba Modelo do devtools. */\\nexport const'\r\n )\r\n}\r\n\r\n/** Layout vazio, pronto para montar pela aba Layout do devtools. */\r\nexport function layoutFileCode(id: string) {\r\n checkLayoutId(id)\r\n const layout: ChartsLayout = { id, filters: [], charts: [] }\r\n return `${JSON.stringify(layout, null, 2)}\\n`\r\n}\r\n\r\n/** `models/index.ts` novo, com só este modelo. */\r\nexport function modelIndexCode(name: string) {\r\n checkModelName(name)\r\n return [\r\n `import { ${name} } from './${name}'`,\r\n '',\r\n '/** Modelos expostos pelo endpoint do dashboard (`runQuery(dashboardModels, ...)`). */',\r\n 'export const dashboardModels = {',\r\n ` ${name}`,\r\n '}',\r\n ''\r\n ].join('\\n')\r\n}\r\n\r\n/**\r\n * Acrescenta o modelo ao `models/index.ts` existente: o import depois do último import e o nome no\r\n * primeiro `export const x = { ... }`. Devolve `null` se o modelo já está registrado.\r\n */\r\nexport function registerModel(source: string, name: string): string | null {\r\n checkModelName(name)\r\n const importPath = new RegExp(`from\\\\s+['\"]\\\\./${name}(\\\\.ts)?['\"]`)\r\n if (importPath.test(source)) return null\r\n\r\n const object = /export\\s+const\\s+\\w+(?:\\s*:[^=]+)?\\s*=\\s*\\{/.exec(source)\r\n if (!object)\r\n throw new ScaffoldError(\r\n 'Não encontrei `export const <nome> = { ... }` no models/index.ts — registre o modelo à mão.'\r\n )\r\n const open = object.index + object[0].length - 1\r\n let depth = 0\r\n let close = -1\r\n for (let i = open; i < source.length; i++) {\r\n if (source[i] === '{') depth++\r\n else if (source[i] === '}' && --depth === 0) {\r\n close = i\r\n break\r\n }\r\n }\r\n if (close === -1) throw new ScaffoldError('Objeto de modelos do models/index.ts sem `}`.')\r\n\r\n const body = source.slice(open + 1, close)\r\n const items = body.trimEnd()\r\n let newBody: string\r\n if (!items.trim()) newBody = `\\n ${name}\\n`\r\n else if (!body.includes('\\n')) newBody = ` ${items.trim().replace(/,$/, '')}, ${name} `\r\n else newBody = `${items.endsWith(',') ? items : `${items},`}\\n ${name}\\n`\r\n let result = `${source.slice(0, open + 1)}${newBody}${source.slice(close)}`\r\n\r\n // Import logo depois do último import do arquivo (ou no topo, se não houver)\r\n const line = `import { ${name} } from './${name}'\\n`\r\n const imports = [...result.matchAll(/^import[\\s\\S]*?from\\s+['\"][^'\"]+['\"];?[^\\n]*\\n/gm)]\r\n const last = imports.at(-1)\r\n const at = last ? last.index + last[0].length : 0\r\n result = `${result.slice(0, at)}${line}${last ? '' : '\\n'}${result.slice(at)}`\r\n return result\r\n}\r\n\r\n// --- página ---\r\n\r\n/** `analiseCusto` → `analise-custo` (rota da página). */\r\nexport const kebabCase = (text: string) =>\r\n text\r\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\r\n .replace(/[_\\s]+/g, '-')\r\n .toLowerCase()\r\n\r\n/** `analiseCusto` → `Analise Custo` (título padrão da página). */\r\nexport const titleCase = (text: string) =>\r\n kebabCase(text)\r\n .split('-')\r\n .filter(Boolean)\r\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\r\n .join(' ')\r\n\r\n/** `analiseCusto` → `AnaliseCustoPage` (nome do componente). */\r\nexport const pageComponentName = (text: string) => `${titleCase(text).replaceAll(' ', '')}Page`\r\n\r\nexport type PageOptions = {\r\n model: string\r\n /** Import do JSON do layout, relativo à página (ex.: `../../dashboard/layouts/vendas.json`). */\r\n layoutImport: string\r\n title: string\r\n /** Endpoint do `runQuery` (padrão `/api/dashboard/query`). */\r\n endpoint: string\r\n /** App Router do Next: a página vira client component (`'use client'`). */\r\n clientDirective?: boolean\r\n /** Import do registro de tipos de gráfico do app (`export const charts`), relativo à página. */\r\n chartsImport?: string\r\n}\r\n\r\n/**\r\n * Valores trocados num template de página do app (`page.template`): o template é a página de um\r\n * dashboard com esses marcadores no lugar do que muda de um para outro.\r\n */\r\nexport const pageTemplateValues = (options: PageOptions): Record<string, string> => ({\r\n __MODEL__: options.model,\r\n __LAYOUT__: options.layoutImport,\r\n __TITLE__: options.title,\r\n __COMPONENT__: pageComponentName(options.model),\r\n __ENDPOINT__: options.endpoint\r\n})\r\n\r\nexport function fillPageTemplate(template: string, options: PageOptions) {\r\n let content = template\r\n for (const [marker, value] of Object.entries(pageTemplateValues(options)))\r\n content = content.replaceAll(marker, value)\r\n return content\r\n}\r\n\r\n/** Página padrão: provider, filtros e gráficos do layout, consultando o endpoint com `fetch`. */\r\nexport function pageFileCode(options: PageOptions) {\r\n const { model, layoutImport, title, endpoint, clientDirective, chartsImport } = options\r\n if (endpoint.includes(\"'\")) throw new ScaffoldError(`Endpoint inválido: ${endpoint}`)\r\n checkModelName(model)\r\n return [\r\n ...(clientDirective ? [\"'use client'\", ''] : []),\r\n 'import {',\r\n ' DashboardCanvas,',\r\n ' DashboardCharts,',\r\n ' type DashboardFetcher,',\r\n ' DashboardFilters,',\r\n ' DashboardProvider',\r\n \"} from '@bluemarble/bi/client'\",\r\n \"import type { ChartsLayout, DashboardResult } from '@bluemarble/bi/types'\",\r\n \"import { Box, Typography } from '@mui/material'\",\r\n ...(chartsImport ? [`import { charts } from '${chartsImport}'`] : []),\r\n `import layout from '${layoutImport}'`,\r\n '',\r\n '/**',\r\n ` * Dashboard do modelo \\`${model}\\`. Filtros, gráficos e o tamanho da tela vêm do layout JSON,`,\r\n ' * editável pela aba Layout do devtools (◆ BI, em desenvolvimento).',\r\n ' */',\r\n '',\r\n `const TITULO = ${literal(title)}`,\r\n 'const chartsLayout = layout as ChartsLayout',\r\n '',\r\n '// Fora do componente: referência estável, senão toda consulta refaria a cada render',\r\n 'const fetcher: DashboardFetcher = async (query) => {',\r\n ` const response = await fetch('${endpoint}', {`,\r\n \" method: 'POST',\",\r\n \" headers: { 'Content-Type': 'application/json' },\",\r\n ' body: JSON.stringify(query)',\r\n ' })',\r\n ' if (!response.ok) throw new Error(await response.text())',\r\n ' return (await response.json()) as DashboardResult',\r\n '}',\r\n '',\r\n `export default function ${pageComponentName(model)}() {`,\r\n ' return (',\r\n ` <DashboardProvider model=\"${model}\" fetcher={fetcher}${chartsImport ? ' charts={charts}' : ''}>`,\r\n ' <Box sx={{ p: 2 }}>',\r\n ' {/* Com `canvas` no JSON, tela de tamanho fixo que escala; sem, responsivo */}',\r\n ' <DashboardCanvas layout={chartsLayout}>',\r\n \" <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>\",\r\n ' <Typography component=\"h1\" variant=\"h5\">',\r\n ' {TITULO}',\r\n ' </Typography>',\r\n ' <DashboardFilters layout={chartsLayout} />',\r\n ' <DashboardCharts layout={chartsLayout} />',\r\n ' </Box>',\r\n ' </DashboardCanvas>',\r\n ' </Box>',\r\n ' </DashboardProvider>',\r\n ' )',\r\n '}',\r\n ''\r\n ].join('\\n')\r\n}\r\n\r\n/**\r\n * Extensão das páginas do Pages Router pelo `pageExtensions` do `next.config` (ex.: `page.tsx` no\r\n * `custo`). Sem a opção (ou sem config), `tsx`.\r\n */\r\nexport function pageExtension(nextConfig: string | undefined) {\r\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('tsx')) ?? 'tsx'\r\n}\r\n\r\n/** Itens do `pageExtensions` do `next.config` (lido como texto). */\r\nfunction pageExtensions(nextConfig: string | undefined) {\r\n const list = /pageExtensions\\s*:\\s*\\[([^\\]]*)\\]/.exec(nextConfig ?? '')?.[1]\r\n return [...(list ?? '').matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1] as string)\r\n}\r\n\r\n// --- endpoint ---\r\n\r\nexport type ApiOptions = {\r\n /** Import do `models/index.ts` (o `dashboardModels`), relativo ao endpoint. */\r\n modelsImport: string\r\n /** Pastas do devtools, relativas à raiz do app (`process.cwd()` no servidor). */\r\n modelsDir: string\r\n layoutsDir: string\r\n snapshotsDir: string\r\n /** Import do registro de tipos de gráfico do servidor (`export const chartTypes`). */\r\n chartTypesImport?: string\r\n /** App Router do Next: `route.ts` com `POST(request)` em vez do handler do Pages Router. */\r\n appRouter?: boolean\r\n}\r\n\r\n/** Valores trocados num template de endpoint do app (`api.template`). */\r\nexport const apiTemplateValues = (options: ApiOptions): Record<string, string> => ({\r\n __MODELS__: options.modelsImport,\r\n __MODELS_DIR__: options.modelsDir,\r\n __LAYOUTS_DIR__: options.layoutsDir,\r\n __SNAPSHOTS_DIR__: options.snapshotsDir\r\n})\r\n\r\nexport function fillApiTemplate(template: string, options: ApiOptions) {\r\n let content = template\r\n for (const [marker, value] of Object.entries(apiTemplateValues(options)))\r\n content = content.replaceAll(marker, value)\r\n return content\r\n}\r\n\r\n/** `src/dashboard/models` → `'src', 'dashboard', 'models'` (argumentos do `path.join`). */\r\nconst pathArgs = (dir: string) =>\r\n dir\r\n .split(/[\\\\/]/)\r\n .filter(Boolean)\r\n .map((part) => literal(part))\r\n .join(', ')\r\n\r\n/**\r\n * Endpoint padrão do `runQuery` com Prisma: um só para todos os dashboards (só aceita os modelos do\r\n * `models/index.ts`), com o devtools ligado só em desenvolvimento.\r\n */\r\nexport function apiFileCode(options: ApiOptions) {\r\n const { modelsImport, chartTypesImport, appRouter } = options\r\n const dir = (path: string) => `path.join(process.cwd(), ${pathArgs(path)})`\r\n const run = (body: string, indent: string) =>\r\n [\r\n 'const result = await runQuery(',\r\n ` dashboardModels,`,\r\n ` ${body} as DashboardQuery,`,\r\n ' (sql, params) => prisma.$queryRawUnsafe<unknown[]>(sql, ...params),',\r\n ' { cacheTtlMs: CACHE_TTL_MS, devtools: DEVTOOLS }',\r\n ')'\r\n ].map((line) => `${indent}${line}`)\r\n return [\r\n \"import path from 'node:path'\",\r\n \"import { DashboardQueryError, runQuery } from '@bluemarble/bi/server'\",\r\n \"import type { DashboardQuery } from '@bluemarble/bi/types'\",\r\n \"import { PrismaClient } from '@prisma/client'\",\r\n ...(appRouter ? [] : [\"import type { NextApiRequest, NextApiResponse } from 'next'\"]),\r\n ...(chartTypesImport ? [`import { chartTypes } from '${chartTypesImport}'`] : []),\r\n `import { dashboardModels } from '${modelsImport}'`,\r\n '',\r\n '// Client próprio: rotas que chamam `prisma.$disconnect()` no client global derrubariam',\r\n '// consultas longas em andamento (\"Engine is not yet connected\").',\r\n 'const globalForPrisma = globalThis as unknown as { dashboardPrisma?: PrismaClient }',\r\n 'const prisma = globalForPrisma.dashboardPrisma ?? new PrismaClient()',\r\n \"if (process.env.NODE_ENV === 'development') globalForPrisma.dashboardPrisma = prisma\",\r\n '',\r\n 'const CACHE_TTL_MS = 5 * 60 * 1000',\r\n '',\r\n '// Devtools (debug, rascunho de modelo, salvar modelo/layout) aceita SQL vindo do browser: só em dev',\r\n \"const DEVTOOLS = process.env.NODE_ENV === 'development' && {\",\r\n ` modelsDir: ${dir(options.modelsDir)},`,\r\n ` snapshotsDir: ${dir(options.snapshotsDir)},`,\r\n ` layoutsDir: ${dir(options.layoutsDir)}${chartTypesImport ? ',' : ''}`,\r\n ...(chartTypesImport ? [' chartTypes'] : []),\r\n '}',\r\n '',\r\n '/**',\r\n ' * Endpoint genérico do `@bluemarble/bi`: recebe `{ model, dimensions, measures, filters }` e',\r\n ' * devolve as linhas agregadas. Só aceita nomes definidos nos modelos registrados.',\r\n ' */',\r\n ...(appRouter\r\n ? [\r\n 'export async function POST(request: Request) {',\r\n ' try {',\r\n ...run('(await request.json())', ' '),\r\n ' return Response.json(result)',\r\n ' } catch (error) {',\r\n ' if (error instanceof DashboardQueryError)',\r\n ' return Response.json({ error: error.message }, { status: 400 })',\r\n ' throw error',\r\n ' }',\r\n '}'\r\n ]\r\n : [\r\n 'export default async function handler(req: NextApiRequest, res: NextApiResponse) {',\r\n \" if (req.method !== 'POST') return res.status(405).setHeader('Allow', 'POST').end()\",\r\n ' try {',\r\n ...run('req.body', ' '),\r\n ' return res.json(result)',\r\n ' } catch (error) {',\r\n ' if (error instanceof DashboardQueryError)',\r\n ' return res.status(400).json({ error: error.message })',\r\n ' throw error',\r\n ' }',\r\n '}'\r\n ]),\r\n ''\r\n ].join('\\n')\r\n}\r\n\r\n/**\r\n * Extensão das rotas de API do Pages Router pelo `pageExtensions` do `next.config` (ex.: `api.ts`\r\n * no `custo`). Sem a opção (ou sem config), `ts`.\r\n */\r\nexport function apiExtension(nextConfig: string | undefined) {\r\n return pageExtensions(nextConfig).find((extension) => extension.endsWith('ts')) ?? 'ts'\r\n}\r\n","#!/usr/bin/env node\r\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\r\nimport { basename, dirname, join, relative, sep } from 'node:path'\r\nimport { parseArgs } from 'node:util'\r\nimport {\r\n type ApiOptions,\r\n apiExtension,\r\n apiFileCode,\r\n checkLayoutId,\r\n checkModelName,\r\n fillApiTemplate,\r\n fillPageTemplate,\r\n kebabCase,\r\n layoutFileCode,\r\n modelFileCode,\r\n modelIndexCode,\r\n type PageOptions,\r\n pageExtension,\r\n pageFileCode,\r\n registerModel,\r\n ScaffoldError,\r\n titleCase\r\n} from './scaffold'\r\n\r\nconst HELP = `Uso: bm-bi new <nome> [opções]\r\n\r\nCria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout\r\n(layouts/<id>.json), a página e, se ainda não existir, o endpoint do runQuery (um só para todos os\r\ndashboards). Depois é montar tudo pelas abas Modelo e Layout do devtools.\r\n\r\nA página vai para o router do Next que existir: src/pages (ou pages) →\r\ndashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou\r\napp) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai\r\ndele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.\r\n\r\nO endpoint vai para a rota do --endpoint no mesmo router: src/pages → api/dashboard/query.<ext> (ext\r\nde API do pageExtensions); src/app → api/dashboard/query/route.ts. O padrão usa Prisma\r\n($queryRawUnsafe); com <dir>/api.template, sai dele, trocando __MODELS__ (import do models/index),\r\n__MODELS_DIR__, __LAYOUTS_DIR__ e __SNAPSHOTS_DIR__ (pastas relativas à raiz do app).\r\n\r\nOpções:\r\n --id <id> id do layout (padrão: o nome do modelo)\r\n --title <texto> título da página (padrão: o id em palavras)\r\n --dir <pasta> pasta dos dashboards (padrão: src/dashboard)\r\n --models <pasta> pasta dos modelos (padrão: <dir>/models)\r\n --layouts <pasta> pasta dos layouts (padrão: <dir>/layouts)\r\n --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)\r\n --skip-page não cria a página\r\n --template <arq.> template da página (padrão: <dir>/page.template, se existir)\r\n --endpoint <url> endpoint do runQuery (padrão: /api/dashboard/query)\r\n --api <arquivo> arquivo do endpoint (padrão: pela rota do --endpoint, ver acima)\r\n --skip-api não cria o endpoint\r\n --api-template <a.> template do endpoint (padrão: <dir>/api.template, se existir)\r\n --snapshots <pasta> pasta dos snapshots no endpoint (padrão: tests/dashboard/snapshots)\r\n -h, --help esta ajuda\r\n`\r\n\r\nconst exists = (path: string) => existsSync(path)\r\n\r\n/** Caminho de import de `to` a partir do arquivo `from` (sempre com `./` ou `../` e `/`). */\r\nfunction importPath(from: string, to: string) {\r\n const path = relative(dirname(from), to).split(sep).join('/')\r\n return path.startsWith('.') ? path : `./${path}`\r\n}\r\n\r\n/** Router do Next que existir no app (Pages primeiro, como o `custo`). */\r\nfunction nextRouter(cwd: string) {\r\n for (const pages of ['src/pages', 'pages']) {\r\n if (!exists(join(cwd, pages))) continue\r\n const config = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs']\r\n .map((file) => join(cwd, file))\r\n .find(exists)\r\n return { app: false, dir: join(cwd, pages), config: config && readFileSync(config, 'utf8') }\r\n }\r\n for (const app of ['src/app', 'app'])\r\n if (exists(join(cwd, app))) return { app: true, dir: join(cwd, app), config: undefined }\r\n return undefined\r\n}\r\n\r\ntype Router = ReturnType<typeof nextRouter>\r\n\r\nfunction defaultPagePath(router: Router, slug: string) {\r\n if (!router) return undefined\r\n return router.app\r\n ? join(router.dir, 'dashboard', slug, 'page.tsx')\r\n : join(router.dir, 'dashboard', slug, `index.${pageExtension(router.config)}`)\r\n}\r\n\r\n/** Endpoint na rota do `--endpoint` (ex.: `/api/dashboard/query`). */\r\nfunction defaultApiPath(router: Router, endpoint: string) {\r\n if (!router) return undefined\r\n if (!/^(\\/[\\w-]+)+$/.test(endpoint))\r\n throw new ScaffoldError(\r\n `Endpoint inválido para criar a rota: ${endpoint} (use --api ou --skip-api)`\r\n )\r\n const route = endpoint.slice(1).split('/')\r\n return router.app\r\n ? join(router.dir, ...route, 'route.ts')\r\n : join(router.dir, ...route.slice(0, -1), `${route.at(-1)}.${apiExtension(router.config)}`)\r\n}\r\n\r\n/** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */\r\nfunction chartsIndex(dir: string) {\r\n return ['index.ts', 'index.tsx']\r\n .map((file) => join(dir, 'charts', file))\r\n .find((path) => exists(path) && /export\\s+const\\s+charts\\b/.test(readFileSync(path, 'utf8')))\r\n}\r\n\r\n/** `export const chartTypes` em <dir>/charts/types.ts: o devtools do endpoint valida o layout. */\r\nfunction chartTypesFile(dir: string) {\r\n return ['types.ts', 'types.tsx']\r\n .map((file) => join(dir, 'charts', file))\r\n .find(\r\n (path) => exists(path) && /export\\s+const\\s+chartTypes\\b/.test(readFileSync(path, 'utf8'))\r\n )\r\n}\r\n\r\n/** Caminho relativo à raiz do app, sempre com `/`. */\r\nconst fromRoot = (cwd: string, path: string) => relative(cwd, path).split(sep).join('/')\r\n\r\nfunction main(argv: string[]) {\r\n const { values, positionals } = parseArgs({\r\n args: argv,\r\n allowPositionals: true,\r\n options: {\r\n id: { type: 'string' },\r\n dir: { type: 'string', default: 'src/dashboard' },\r\n models: { type: 'string' },\r\n layouts: { type: 'string' },\r\n title: { type: 'string' },\r\n page: { type: 'string' },\r\n 'skip-page': { type: 'boolean' },\r\n template: { type: 'string' },\r\n endpoint: { type: 'string', default: '/api/dashboard/query' },\r\n api: { type: 'string' },\r\n 'skip-api': { type: 'boolean' },\r\n 'api-template': { type: 'string' },\r\n snapshots: { type: 'string', default: 'tests/dashboard/snapshots' },\r\n help: { type: 'boolean', short: 'h' }\r\n }\r\n })\r\n const [command, name, ...rest] = positionals\r\n if (values.help || !command) {\r\n process.stdout.write(HELP)\r\n return\r\n }\r\n if (command !== 'new') throw new ScaffoldError(`Comando desconhecido: ${command}\\n\\n${HELP}`)\r\n if (!name) throw new ScaffoldError(`Falta o nome do modelo.\\n\\n${HELP}`)\r\n if (rest.length) throw new ScaffoldError(`Argumentos a mais: ${rest.join(' ')}`)\r\n\r\n const id = values.id ?? name\r\n checkModelName(name)\r\n checkLayoutId(id)\r\n\r\n const cwd = process.cwd()\r\n const modelsDir = join(cwd, values.models ?? join(values.dir, 'models'))\r\n const layoutsDir = join(cwd, values.layouts ?? join(values.dir, 'layouts'))\r\n const modelPath = join(modelsDir, `${name}.ts`)\r\n const indexPath = join(modelsDir, 'index.ts')\r\n const layoutPath = join(layoutsDir, `${id}.json`)\r\n const dashboardDir = join(cwd, values.dir)\r\n const router = nextRouter(cwd)\r\n const pagePath = values['skip-page']\r\n ? undefined\r\n : values.page\r\n ? join(cwd, values.page)\r\n : defaultPagePath(router, kebabCase(id))\r\n const apiPath = values['skip-api']\r\n ? undefined\r\n : values.api\r\n ? join(cwd, values.api)\r\n : defaultApiPath(router, values.endpoint)\r\n // O endpoint é um só para todos os dashboards: se já existe, fica como está\r\n const apiExists = !!apiPath && exists(apiPath)\r\n const templatePath = values.template\r\n ? join(cwd, values.template)\r\n : [join(dashboardDir, 'page.template')].find(exists)\r\n const apiTemplatePath = values['api-template']\r\n ? join(cwd, values['api-template'])\r\n : [join(dashboardDir, 'api.template')].find(exists)\r\n for (const path of [templatePath, apiTemplatePath])\r\n if (path && !exists(path))\r\n throw new ScaffoldError(`Template não encontrado: ${relative(cwd, path)}`)\r\n\r\n // Confere tudo antes de escrever: ou cria o dashboard inteiro, ou nada\r\n for (const path of [modelPath, layoutPath, pagePath])\r\n if (path && existsSync(path)) throw new ScaffoldError(`Já existe: ${relative(cwd, path)}`)\r\n const index = existsSync(indexPath)\r\n ? registerModel(readFileSync(indexPath, 'utf8'), name)\r\n : modelIndexCode(name)\r\n let page: string | undefined\r\n if (pagePath) {\r\n const charts = chartsIndex(dashboardDir)\r\n const options: PageOptions = {\r\n model: name,\r\n layoutImport: importPath(pagePath, layoutPath),\r\n title: values.title ?? titleCase(id),\r\n endpoint: values.endpoint,\r\n // App Router: a página usa hooks, então é client component\r\n clientDirective:\r\n basename(pagePath).startsWith('page.') &&\r\n relative(cwd, pagePath).split(sep).includes('app'),\r\n chartsImport: charts && importPath(pagePath, dirname(charts))\r\n }\r\n page = templatePath\r\n ? fillPageTemplate(readFileSync(templatePath, 'utf8'), options)\r\n : pageFileCode(options)\r\n }\r\n let api: string | undefined\r\n if (apiPath && !apiExists) {\r\n const chartTypes = chartTypesFile(dashboardDir)\r\n const options: ApiOptions = {\r\n modelsImport: importPath(apiPath, modelsDir),\r\n modelsDir: fromRoot(cwd, modelsDir),\r\n layoutsDir: fromRoot(cwd, layoutsDir),\r\n snapshotsDir: fromRoot(cwd, join(cwd, values.snapshots)),\r\n chartTypesImport: chartTypes && importPath(apiPath, chartTypes.replace(/\\.tsx?$/, '')),\r\n appRouter: basename(apiPath).startsWith('route.')\r\n }\r\n api = apiTemplatePath\r\n ? fillApiTemplate(readFileSync(apiTemplatePath, 'utf8'), options)\r\n : apiFileCode(options)\r\n }\r\n\r\n const written: string[] = []\r\n const write = (path: string, content: string) => {\r\n mkdirSync(dirname(path), { recursive: true })\r\n writeFileSync(path, content)\r\n written.push(relative(cwd, path))\r\n }\r\n write(modelPath, modelFileCode(name))\r\n if (index !== null) write(indexPath, index)\r\n write(layoutPath, layoutFileCode(id))\r\n if (pagePath && page !== undefined) write(pagePath, page)\r\n if (apiPath && api !== undefined) write(apiPath, api)\r\n\r\n process.stdout.write(\r\n [\r\n ...written.map((path) => `✓ ${path}`),\r\n ...(pagePath\r\n ? templatePath\r\n ? [` (página do template ${relative(cwd, templatePath)})`]\r\n : []\r\n : values['skip-page']\r\n ? []\r\n : [\r\n '',\r\n 'Página não criada: não achei src/pages nem src/app. Use --page <arquivo>, com o layout:',\r\n ` <DashboardProvider model=\"${name}\" fetcher={fetcher}>`,\r\n ' <DashboardFilters layout={layout} />',\r\n ' <DashboardCharts layout={layout} />',\r\n ' </DashboardProvider>'\r\n ]),\r\n ...(apiPath\r\n ? apiExists\r\n ? [`· ${relative(cwd, apiPath)} (endpoint já existe, mantido)`]\r\n : apiTemplatePath\r\n ? [` (endpoint do template ${relative(cwd, apiTemplatePath)})`]\r\n : []\r\n : values['skip-api']\r\n ? []\r\n : ['', 'Endpoint não criado: não achei src/pages nem src/app. Use --api <arquivo>.']),\r\n ''\r\n ].join('\\n')\r\n )\r\n}\r\n\r\ntry {\r\n main(process.argv.slice(2))\r\n} catch (error) {\r\n if (!(error instanceof ScaffoldError) && !(error instanceof TypeError)) throw error\r\n process.stderr.write(`${error.message}\\n`)\r\n process.exitCode = 1\r\n}\r\n"],"mappings":"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"}
|