@bluemarble/bi 0.6.4 → 0.6.6

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.
@@ -1,6 +1,6 @@
1
- import { s as AGGREGATION_LABEL, t as canonicalJson, u as isAggregate } from "./codegen-DkfI6UJr.js";
2
- import { g as resolveChartTypes } from "./layout-nViz3MdU.js";
3
- import { Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
1
+ import { s as AGGREGATION_LABEL, t as canonicalJson, u as isAggregate } from "./codegen-LoQzFAle.js";
2
+ import { _ as resolveChartTypes } from "./layout-CrZ00tB0.js";
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";
6
6
  //#region src/devtools/layoutStore.ts
@@ -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-CSRt9lib.js"));
286
+ const DashboardDevtools = lazy(() => import("./DashboardDevtools-I00Iz0Z8.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) {
@@ -411,6 +411,123 @@ function useDashboard() {
411
411
  return context;
412
412
  }
413
413
  //#endregion
414
+ //#region src/client/chartKit.tsx
415
+ /**
416
+ * Peças comuns dos gráficos ligados ao modelo: paleta, filtro por clique, legenda e os estados de
417
+ * carregando/erro/vazio.
418
+ */
419
+ /**
420
+ * Paleta categórica, sempre nesta ordem (a série N tem sempre a mesma cor). Laranja Rocha primeiro;
421
+ * a ordem passa no validador de daltonismo (CVD ΔE ≥ 9 entre vizinhas) e de visão normal (≥ 19).
422
+ */
423
+ const SERIES_COLORS = [
424
+ "#F58220",
425
+ "#2a78d6",
426
+ "#1baf7a",
427
+ "#eda100",
428
+ "#e87ba4",
429
+ "#008300",
430
+ "#6250d6",
431
+ "#e34948"
432
+ ];
433
+ /** "Outros" (fatias agrupadas) fica neutro, fora da paleta. */
434
+ const OTHER_COLOR = "#9e9e9e";
435
+ const seriesColor = (index) => SERIES_COLORS[index % SERIES_COLORS.length];
436
+ /** Valor numérico de uma coluna da linha (`null` quando vazio). */
437
+ const numberOf = (row, key) => key && row[key] !== null && row[key] !== void 0 ? Number(row[key]) : null;
438
+ /**
439
+ * Filtro cruzado a partir do gráfico: clicar num valor filtra o dashboard por ele (Ctrl/Cmd+clique
440
+ * acumula; clicar de novo no único selecionado limpa). O gráfico deve ignorar o próprio filtro na
441
+ * consulta (`ignoreFilters`) e só destacar a seleção.
442
+ */
443
+ function useClickFilter(dimension, enabled) {
444
+ const { filters, setFilter } = useDashboard();
445
+ const selected = enabled && dimension ? filters[dimension] ?? [] : [];
446
+ const toggle = (value, event) => {
447
+ if (!enabled || !dimension) return;
448
+ if (event?.ctrlKey || event?.metaKey) setFilter(dimension, selected.includes(value) ? selected.filter((item) => item !== value) : [...selected, value]);
449
+ else setFilter(dimension, selected.length === 1 && selected[0] === value ? [] : [value]);
450
+ };
451
+ return {
452
+ selected,
453
+ /** Há seleção e este valor não está nela. */
454
+ isDimmed: (value) => selected.length > 0 && !selected.includes(value),
455
+ toggle,
456
+ clear: () => {
457
+ if (enabled && dimension && selected.length) setFilter(dimension, []);
458
+ }
459
+ };
460
+ }
461
+ function Legend({ items, loading }) {
462
+ return /* @__PURE__ */ jsxs(Box, {
463
+ sx: {
464
+ display: "flex",
465
+ flexWrap: "wrap",
466
+ columnGap: 1.5,
467
+ rowGap: .5,
468
+ mb: 1,
469
+ fontSize: 14,
470
+ color: "#555",
471
+ minHeight: 21
472
+ },
473
+ children: [items.map((item) => /* @__PURE__ */ jsxs(Box, {
474
+ sx: {
475
+ display: "flex",
476
+ alignItems: "center",
477
+ gap: .5
478
+ },
479
+ children: [/* @__PURE__ */ jsx(Box, { sx: {
480
+ width: 9,
481
+ height: 9,
482
+ borderRadius: "50%",
483
+ bgcolor: item.color
484
+ } }), item.label]
485
+ }, item.label)), loading && /* @__PURE__ */ jsx(CircularProgress, {
486
+ size: 14,
487
+ sx: { ml: "auto" }
488
+ })]
489
+ });
490
+ }
491
+ /** Erro / carregando / sem dados no lugar do gráfico; `null` quando há o que desenhar (função, não componente). */
492
+ function chartPlaceholder({ height, error, isLoading, empty }) {
493
+ if (!error && !empty) return null;
494
+ return /* @__PURE__ */ jsx(Box, {
495
+ sx: {
496
+ height,
497
+ display: "flex",
498
+ alignItems: "center",
499
+ justifyContent: "center",
500
+ color: error ? "error.main" : "#999"
501
+ },
502
+ children: error ? "Erro ao carregar o gráfico" : isLoading ? "Carregando…" : "Sem dados para os filtros selecionados"
503
+ });
504
+ }
505
+ /** `caption`: o que é o título (ex.: o rótulo da dimensão), em cinza acima dele. */
506
+ function TooltipBox({ title, caption, children }) {
507
+ return /* @__PURE__ */ jsxs(Box, {
508
+ sx: {
509
+ bgcolor: "#fff",
510
+ border: "1px solid #ccc",
511
+ p: 1,
512
+ fontSize: 13
513
+ },
514
+ children: [
515
+ caption && /* @__PURE__ */ jsx(Box, {
516
+ sx: {
517
+ fontSize: 11,
518
+ color: "#777"
519
+ },
520
+ children: caption
521
+ }),
522
+ /* @__PURE__ */ jsx(Box, {
523
+ sx: { fontWeight: 700 },
524
+ children: title
525
+ }),
526
+ children
527
+ ]
528
+ });
529
+ }
530
+ //#endregion
414
531
  //#region src/client/format.ts
415
532
  const formatters = /* @__PURE__ */ new Map();
416
533
  /** `compact`: notação curta do pt-BR (1,2 mil / 3,4 mi / 5,6 bi). */
@@ -624,6 +741,6 @@ function ModelDictionary() {
624
741
  });
625
742
  }
626
743
  //#endregion
627
- export { formatCurrency as a, measureFormat as c, useDashboard as d, errorMessage as f, parseLayoutItemKey as h, fieldLabel as i, measureFormatter as l, layoutItemKey as m, INSPECT_ATTRIBUTE as n, formatMeasure as o, LAYOUT_ITEM_ATTRIBUTE as p, useModelQuery as r, formatNumber as s, ModelDictionary as t, DashboardProvider as u };
744
+ export { parseLayoutItemKey as C, layoutItemKey as S, useClickFilter as _, formatCurrency as a, errorMessage as b, measureFormat as c, OTHER_COLOR as d, SERIES_COLORS as f, seriesColor as g, numberOf as h, fieldLabel as i, measureFormatter as l, chartPlaceholder as m, INSPECT_ATTRIBUTE as n, formatMeasure as o, TooltipBox as p, useModelQuery as r, formatNumber as s, ModelDictionary as t, Legend as u, DashboardProvider as v, LAYOUT_ITEM_ATTRIBUTE as x, useDashboard as y };
628
745
 
629
- //# sourceMappingURL=ModelDictionary-Dib4Vk3i.js.map
746
+ //# sourceMappingURL=ModelDictionary-DUGFHBo-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelDictionary-DUGFHBo-.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'\nimport type { ChartsLayout } from '../layout'\n\n/**\n * Layouts montados na página (`<DashboardCharts>` / `<DashboardFilters>`) e o rascunho de cada um,\n * editado na aba Layout do devtools. Store externo como o de consultas: editar um gráfico\n * re-renderiza só os componentes daquele layout e o painel.\n */\n\n/** Parte do layout que um componente renderiza. */\nexport type LayoutPart = 'charts' | 'filters' | 'canvas'\n\nexport type LayoutEntry = {\n /** O layout que veio do arquivo (o que a página passou ao `<DashboardCharts>`). */\n registered: ChartsLayout\n /** Rascunho editado no devtools; `null` = página usando o registrado. */\n draft: ChartsLayout | null\n /**\n * Partes montadas na página e os formatters que cada uma recebeu (`formatDimension`,\n * `formatLabel`). Parte ausente = a página não renderiza (ex.: sem `<DashboardFilters>`).\n */\n parts: Partial<Record<LayoutPart, string[]>>\n}\n\n/**\n * Marca o elemento de cada item do layout com `<layoutId>/chart:<id>` ou\n * `<layoutId>/filter:<dimensão>` (destaque e inspetor do devtools).\n */\nexport const LAYOUT_ITEM_ATTRIBUTE = 'data-bm-dashboard-layout-item'\n\nexport const layoutItemKey = (layoutId: string, item: LayoutItem) =>\n `${layoutId}/${item.kind}:${item.key}`\n\nexport type LayoutItem = { kind: 'chart' | 'filter'; key: string }\n\nexport function parseLayoutItemKey(value: string): { layoutId: string; item: LayoutItem } | null {\n const match = /^([^/]+)\\/(chart|filter):(.+)$/.exec(value)\n return match\n ? {\n layoutId: match[1] as string,\n item: { kind: match[2] as LayoutItem['kind'], key: match[3] as string }\n }\n : null\n}\n\nconst draftKey = (id: string) => `bm-dashboard-devtools:layout:${id}`\n\nconst sameLayout = (a: ChartsLayout | null, b: ChartsLayout | null) =>\n canonicalJson(a) === canonicalJson(b)\n\nfunction loadDraft(id: string): ChartsLayout | null {\n try {\n const saved = sessionStorage.getItem(draftKey(id))\n return saved ? (JSON.parse(saved) as ChartsLayout) : null\n } catch {\n return null\n }\n}\n\nfunction storeDraft(id: string, draft: ChartsLayout | null) {\n try {\n if (draft) sessionStorage.setItem(draftKey(id), JSON.stringify(draft))\n else sessionStorage.removeItem(draftKey(id))\n } catch {}\n}\n\nexport function createLayoutStore() {\n const entries = new Map<string, LayoutEntry>()\n // Montagens por layout e parte (`<id>\\u0000<parte>`)\n const mounts = new Map<string, number>()\n let snapshot: LayoutEntry[] = []\n const listeners = new Set<() => void>()\n // Edições da aba Layout ainda não aplicadas: sobrevivem a trocar de aba (o painel desmonta)\n const pending = new Map<string, ChartsLayout>()\n\n const emit = () => {\n snapshot = [...entries.values()]\n for (const listener of listeners) listener()\n }\n\n const setDraft = (id: string, next: ChartsLayout | null) => {\n const entry = entries.get(id)\n if (!entry) return\n // Rascunho igual ao arquivo não é rascunho (ex.: desfez tudo à mão)\n const draft = next && !sameLayout(next, entry.registered) ? next : null\n storeDraft(id, draft)\n entries.set(id, { ...entry, draft })\n emit()\n }\n\n return {\n subscribe(listener: () => void) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n getSnapshot: () => snapshot,\n get: (id: string) => entries.get(id) ?? null,\n /** Rascunho do layout, mesmo antes de registrar (evita renderizar o registrado e trocar). */\n draftOf(layout: ChartsLayout) {\n const entry = entries.get(layout.id)\n if (entry) return entry.draft\n const draft = loadDraft(layout.id)\n return draft && !sameLayout(draft, layout) ? draft : null\n },\n\n /** Chamado pelo `<DashboardCharts>`/`<DashboardFilters>` ao montar e quando o arquivo muda (HMR). */\n register(layout: ChartsLayout, part: LayoutPart, formatters: string[]) {\n const current = entries.get(layout.id)\n let draft = current ? current.draft : loadDraft(layout.id)\n // Depois de \"Salvar no arquivo\" o Next recarrega o JSON: o rascunho virou o registrado\n if (draft && sameLayout(draft, layout)) {\n draft = null\n storeDraft(layout.id, null)\n }\n const parts = { ...current?.parts, [part]: formatters }\n entries.set(layout.id, { registered: layout, draft, parts })\n const mountKey = `${layout.id}\\u0000${part}`\n mounts.set(mountKey, (mounts.get(mountKey) ?? 0) + 1)\n emit()\n return () => {\n const count = (mounts.get(mountKey) ?? 1) - 1\n mounts.set(mountKey, count)\n const entry = entries.get(layout.id)\n if (count > 0 || !entry) return\n const { [part]: _, ...rest } = entry.parts\n // O rascunho continua no sessionStorage e volta na próxima montagem\n if (Object.keys(rest).length) entries.set(layout.id, { ...entry, parts: rest })\n else entries.delete(layout.id)\n emit()\n }\n },\n setDraft,\n /** Edições ainda não aplicadas do layout (só em memória). */\n pendingOf: (id: string) => pending.get(id) ?? null,\n setPending(id: string, layout: ChartsLayout | null) {\n if (layout) pending.set(id, layout)\n else pending.delete(id)\n }\n }\n}\n\nexport type LayoutStore = ReturnType<typeof createLayoutStore>\n","import type { DashboardQuery, DashboardResult } from '../types'\n\ntype Fetcher = (query: DashboardQuery) => Promise<DashboardResult>\n\ntype Pending = {\n query: DashboardQuery\n resolve: (result: DashboardResult) => void\n reject: (error: unknown) => void\n}\n\n/**\n * Manda num request só (`{ model, queries }`) as consultas de um modelo feitas no mesmo ciclo — as\n * de um render da página. O servidor junta numa SQL só as que dão para juntar (mesmas dimensões,\n * filtros e fatos; ver `runBatch`): os KPIs viram uma consulta com todas as medidas, e \"Vol. Total\"\n * (fórmula de \"Vol. Convencional\" + \"Vol. Esteira\") reaproveita as agregações dos outros dois.\n *\n * Consultas do devtools (`query.devtools`) e de metadados passam direto. Se o lote falhar (ou o\n * servidor for de uma versão sem lote), cada consulta é refeita sozinha.\n */\nexport function batchQueries(fetcher: Fetcher): Fetcher {\n const batches = new Map<string, Pending[]>()\n\n const alone = ({ query, resolve, reject }: Pending) => fetcher(query).then(resolve, reject)\n\n const flush = (model: string) => {\n const members = batches.get(model) ?? []\n batches.delete(model)\n if (members.length === 1) return alone(members[0])\n const queries = members.map(({ query: { dimensions, measures, filters } }) => ({\n dimensions,\n measures,\n filters\n }))\n fetcher({ model, queries }).then(\n ({ results }) => {\n if (results?.length !== members.length) return members.forEach(alone)\n members.forEach(({ resolve, reject }, index) => {\n const { error, ...result } = results[index]\n if (error) reject(new Error(error))\n else resolve(result)\n })\n },\n () => members.forEach(alone)\n )\n }\n\n return (query) => {\n if (query.meta || query.devtools || query.queries) return fetcher(query)\n return new Promise((resolve, reject) => {\n const members = batches.get(query.model)\n if (members) return void members.push({ query, resolve, reject })\n batches.set(query.model, [{ query, resolve, reject }])\n // As consultas de uma renderização disparam todas nos efeitos do mesmo commit\n setTimeout(() => flush(query.model), 0)\n })\n }\n}\n","import type { DashboardQuery, DashboardQueryDebug, DashboardRow } from '../types'\n\n/**\n * Registro das consultas feitas pelos componentes (`useModelQuery`), lido pelo painel do devtools.\n * É um store externo (`useSyncExternalStore`) para que registrar uma consulta não re-renderize a\n * página inteira — só o painel.\n */\n\nexport type RunStatus = 'loading' | 'success' | 'error' | 'cancelled'\n\nexport type QueryRun = {\n runId: number\n instanceId: string\n label: string\n query: DashboardQuery\n status: RunStatus\n startedAt: number\n /** Ida e volta medida no browser. */\n durationMs?: number\n rows?: DashboardRow[]\n error?: string\n debug?: DashboardQueryDebug\n /** Foi num request junto com outras consultas (quantas, contando ela); o `debug` é do conjunto. */\n batched?: number\n}\n\nexport type TrackedInstance = {\n instanceId: string\n label: string\n runs: number\n last: QueryRun\n /** Refeita muitas vezes em pouco tempo: sinal de re-render em loop (ex.: array novo a cada render). */\n storm: boolean\n}\n\nexport type DevtoolsSnapshot = {\n instances: TrackedInstance[]\n /** Execuções mais recentes primeiro. */\n history: QueryRun[]\n}\n\nconst MAX_HISTORY = 100\n/** `STORM_RUNS` execuções da mesma instância dentro de `STORM_WINDOW_MS` = consulta em loop. */\nconst STORM_RUNS = 6\nconst STORM_WINDOW_MS = 5000\n\nexport function errorMessage(error: unknown): string {\n const err = error as {\n response?: { data?: { error?: string; message?: string } }\n message?: string\n }\n return err?.response?.data?.error ?? err?.response?.data?.message ?? err?.message ?? String(error)\n}\n\nexport function createDevtoolsStore() {\n const instances = new Map<string, TrackedInstance>()\n let history: QueryRun[] = []\n // Execuções em andamento (o histórico é limitado e pode já ter descartado uma consulta lenta)\n const pending = new Map<number, QueryRun>()\n const recentStarts = new Map<string, number[]>()\n // Re-execuções pedidas pelo próprio devtools (refresh, aplicar rascunho) não contam como loop\n let expectedUntil = 0\n let snapshot: DevtoolsSnapshot = { instances: [], history: [] }\n let nextRunId = 1\n const listeners = new Set<() => void>()\n\n const emit = () => {\n snapshot = { instances: [...instances.values()], history }\n for (const listener of listeners) listener()\n }\n\n const update = (runId: number, patch: Partial<QueryRun>) => {\n const current = pending.get(runId)\n if (!current) return\n pending.delete(runId)\n const run = { ...current, ...patch, durationMs: Date.now() - current.startedAt }\n history = history.map((item) => (item.runId === runId ? run : item))\n const instance = instances.get(run.instanceId)\n if (instance?.last.runId === runId) instances.set(run.instanceId, { ...instance, last: run })\n emit()\n }\n\n return {\n subscribe(listener: () => void) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n getSnapshot: () => snapshot,\n\n start(instanceId: string, label: string, query: DashboardQuery) {\n const run: QueryRun = {\n runId: nextRunId++,\n instanceId,\n label,\n query,\n status: 'loading',\n startedAt: Date.now()\n }\n pending.set(run.runId, run)\n history = [run, ...history].slice(0, MAX_HISTORY)\n const previous = instances.get(instanceId)\n const expected = run.startedAt <= expectedUntil\n const starts = [\n ...(recentStarts.get(instanceId) ?? []),\n ...(expected ? [] : [run.startedAt])\n ].filter((time) => run.startedAt - time <= STORM_WINDOW_MS)\n recentStarts.set(instanceId, starts)\n const storm = previous?.storm || starts.length >= STORM_RUNS\n if (storm && !previous?.storm)\n console.warn(\n `[@bluemarble/bi] \"${label}\" executou ${starts.length}x em ${STORM_WINDOW_MS / 1000}s. ` +\n 'Confira se o componente não recria dimensions/measures/filtros a cada render.'\n )\n instances.set(instanceId, {\n instanceId,\n label,\n runs: (previous?.runs ?? 0) + 1,\n last: run,\n storm\n })\n emit()\n return run.runId\n },\n succeed: (runId: number, rows: DashboardRow[], debug?: DashboardQueryDebug, batched?: number) =>\n update(runId, { status: 'success', rows, debug, batched }),\n fail: (runId: number, error: unknown) =>\n update(runId, { status: 'error', error: errorMessage(error) }),\n /** Consulta descartada antes de responder (filtro mudou, componente desmontou). */\n cancel: (runId: number) => update(runId, { status: 'cancelled' }),\n\n remove(instanceId: string) {\n recentStarts.delete(instanceId)\n if (instances.delete(instanceId)) emit()\n },\n /** Avisa que todas as consultas vão refazer agora, de propósito. */\n expectRefetch() {\n expectedUntil = Date.now() + 500\n },\n /** Zera os alertas de loop (depois de corrigir o componente, sem recarregar a página). */\n resetStorms() {\n recentStarts.clear()\n for (const [id, instance] of instances) instances.set(id, { ...instance, storm: false })\n emit()\n },\n clearHistory() {\n history = []\n emit()\n }\n }\n}\n\nexport type DevtoolsStore = ReturnType<typeof createDevtoolsStore>\n","import {\n type ComponentType,\n createContext,\n lazy,\n type ReactNode,\n Suspense,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState\n} from 'react'\nimport { createLayoutStore, type LayoutStore } from '../devtools/layoutStore'\nimport { batchQueries } from './batchQueries'\nimport { createDevtoolsStore, type DevtoolsStore } from '../devtools/store'\nimport { type ChartTypeSpec, type ChartTypes, resolveChartTypes } from '../layout'\nimport type { ModelDef, ModelMeta } from '../model'\nimport type { DashboardFilters, DashboardQuery, DashboardResult, FilterValue } from '../types'\n\nexport type DashboardFetcher = (query: DashboardQuery) => Promise<DashboardResult>\n\n/**\n * Tipo de gráfico do app: os campos do JSON (`ChartTypeSpec`) e o componente que os renderiza. O\n * componente recebe os campos como props (com os `default` aplicados e formatters já resolvidos) e\n * `queryLabel` — ver `ChartComponentProps`.\n */\n// biome-ignore lint/suspicious/noExplicitAny: cada tipo tem as próprias props, vindas do JSON\nexport type ChartComponentSpec = ChartTypeSpec & { component: ComponentType<any> }\n\n/** Props de um componente registrado em `charts`: os campos do tipo + `queryLabel`. */\nexport type ChartComponentProps<Fields extends object = Record<string, unknown>> = Fields & {\n /** Nome da consulta no devtools (`<título> · <tipo>`): passe ao `useModelQuery({ label })`. */\n queryLabel: string\n}\n\n/** Só existe com o devtools ligado. */\nexport type DashboardDevtoolsContext = {\n store: DevtoolsStore\n /** Layouts da página (`<DashboardCharts>`/`<DashboardFilters>`) e os rascunhos da aba Layout. */\n layouts: LayoutStore\n /** Consulta o modelo registrado, ignorando o rascunho (comparações, snapshots). */\n rawFetcher: DashboardFetcher\n /** Rascunho do modelo aplicado: todas as consultas da página passam a usá-lo. */\n draft: ModelDef | null\n setDraft: (draft: ModelDef | null) => void\n /** Refaz todas as consultas da página. */\n refresh: () => void\n defaultFilters: DashboardFilters\n setFilters: (filters: DashboardFilters) => void\n}\n\ntype DashboardContextValue = {\n model: string\n filters: DashboardFilters\n setFilter: (dimension: string, values: FilterValue[]) => void\n fetcher: DashboardFetcher\n /** Dicionário do modelo (rótulos, descrições, formatos); `null` até carregar. */\n meta: ModelMeta | null\n /** Muda quando o devtools pede para refazer as consultas. */\n refreshKey: number\n /** Tipos de gráfico do layout: os da lib e os do app (`charts`). */\n chartTypes: ChartTypes\n /** Componentes dos tipos do app, por nome. */\n chartComponents: Record<string, ChartComponentSpec['component']>\n devtools: DashboardDevtoolsContext | null\n}\n\nconst DashboardContext = createContext<DashboardContextValue | null>(null)\n\n// Lazy: o painel não entra no bundle da página, só é baixado quando o devtools está ligado\nconst DashboardDevtools = lazy(() => import('../devtools/DashboardDevtools'))\n\nconst DEVTOOLS_DEFAULT = process.env.NODE_ENV === 'development'\n\nconst draftStorageKey = (model: string) => `bm-dashboard-devtools:draft:${model}`\n\nfunction loadDraft(model: string): ModelDef | null {\n try {\n const saved = sessionStorage.getItem(draftStorageKey(model))\n return saved ? (JSON.parse(saved) as ModelDef) : null\n } catch {\n return null\n }\n}\n\ntype DashboardProviderProps = {\n /** Nome do modelo no servidor (chave do registro de modelos do app). */\n model: string\n /** Como chamar o endpoint do app (ex.: `(q) => api.post('/dashboard/query', q).then((r) => r.data)`). */\n fetcher: DashboardFetcher\n defaultFilters?: DashboardFilters\n /**\n * Painel de desenvolvimento (debug das consultas, edição do modelo). Padrão: ligado só com\n * `NODE_ENV=development`. O endpoint também precisa de `runQuery(..., { devtools: true })`.\n */\n devtools?: boolean\n /**\n * Tipos de gráfico do app, usáveis no layout JSON (`\"type\": \"<nome>\"`) e no editor do devtools.\n * Declare fora do componente (referência estável). O servidor precisa dos mesmos campos em\n * `devtools.chartTypes` para salvar o layout.\n */\n charts?: Record<string, ChartComponentSpec>\n /**\n * Manda as consultas de um render num request só; o servidor junta numa SQL as que têm as mesmas\n * dimensões, filtros e fatos (ex.: os KPIs). Padrão: ligado.\n */\n batch?: boolean\n children: ReactNode\n}\n\n/**\n * Guarda os filtros do dashboard. Todo componente dentro dele (filtros, gráficos, tabelas) consulta o\n * mesmo modelo com os mesmos filtros — é o que faz um slicer filtrar a página inteira, como no BI.\n */\nexport function DashboardProvider({\n model,\n fetcher,\n defaultFilters = {},\n devtools = DEVTOOLS_DEFAULT,\n charts,\n batch = true,\n children\n}: DashboardProviderProps) {\n const chartTypes = useMemo(() => resolveChartTypes(charts), [charts])\n const chartComponents = useMemo(\n () =>\n Object.fromEntries(\n Object.entries(charts ?? {}).map(([name, spec]) => [name, spec.component])\n ),\n [charts]\n )\n const [filters, setFilters] = useState<DashboardFilters>(defaultFilters)\n const defaultsRef = useRef(defaultFilters)\n\n const setFilter = useCallback(\n (dimension: string, values: FilterValue[]) =>\n setFilters((current) => ({ ...current, [dimension]: values })),\n []\n )\n\n // --- devtools ---\n const store = useMemo(() => (devtools ? createDevtoolsStore() : null), [devtools])\n const layouts = useMemo(() => (devtools ? createLayoutStore() : null), [devtools])\n // O rascunho aplicado sobrevive ao reload da aba (útil enquanto se edita o modelo)\n const [draft, setDraftState] = useState<ModelDef | null>(() =>\n devtools && typeof window !== 'undefined' ? loadDraft(model) : null\n )\n const [refreshKey, setRefreshKey] = useState(0)\n const [mounted, setMounted] = useState(false)\n useEffect(() => setMounted(true), [])\n\n const setDraft = useCallback(\n (next: ModelDef | null) => {\n store?.expectRefetch()\n setDraftState(next)\n try {\n if (next) sessionStorage.setItem(draftStorageKey(model), JSON.stringify(next))\n else sessionStorage.removeItem(draftStorageKey(model))\n } catch {}\n },\n [model, store]\n )\n\n const refresh = useCallback(() => {\n store?.expectRefetch()\n setRefreshKey((key) => key + 1)\n }, [store])\n\n // Com o devtools ligado, toda consulta pede debug e, se houver, roda contra o rascunho\n const effectiveFetcher = useMemo<DashboardFetcher>(() => {\n if (!devtools) return fetcher\n return (query) =>\n fetcher({\n ...query,\n devtools: { debug: true, ...(draft && { model: draft }), ...query.devtools }\n })\n }, [fetcher, devtools, draft])\n const queryFetcher = useMemo(\n () => (batch ? batchQueries(effectiveFetcher) : effectiveFetcher),\n [batch, effectiveFetcher]\n )\n\n const rawFetcher = useMemo<DashboardFetcher>(\n () => (query) => fetcher({ ...query, devtools: { debug: true, ...query.devtools } }),\n [fetcher]\n )\n\n // Dicionário: segue o rascunho, para os rótulos editados aparecerem na hora\n const [meta, setMeta] = useState<ModelMeta | null>(null)\n useEffect(() => {\n let cancelado = false\n effectiveFetcher({ model, meta: true })\n .then((result) => !cancelado && setMeta(result.meta ?? null))\n .catch(() => !cancelado && setMeta(null))\n return () => {\n cancelado = true\n }\n }, [effectiveFetcher, model])\n\n const devtoolsValue = useMemo(\n () =>\n store &&\n layouts && {\n store,\n layouts,\n rawFetcher,\n draft,\n setDraft,\n refresh,\n defaultFilters: defaultsRef.current,\n setFilters\n },\n [store, layouts, rawFetcher, draft, setDraft, refresh]\n )\n\n const value = useMemo(\n () => ({\n model,\n filters,\n setFilter,\n fetcher: queryFetcher,\n meta,\n refreshKey,\n chartTypes,\n chartComponents,\n devtools: devtoolsValue\n }),\n [\n model,\n filters,\n setFilter,\n queryFetcher,\n meta,\n refreshKey,\n chartTypes,\n chartComponents,\n devtoolsValue\n ]\n )\n\n return (\n <DashboardContext.Provider value={value}>\n {children}\n {/* Só depois de montar: o painel lê localStorage e não deve participar da hidratação */}\n {devtoolsValue && mounted && (\n <Suspense fallback={null}>\n <DashboardDevtools />\n </Suspense>\n )}\n </DashboardContext.Provider>\n )\n}\n\nexport function useDashboard() {\n const context = useContext(DashboardContext)\n if (!context) throw new Error('useDashboard precisa estar dentro de <DashboardProvider>')\n return context\n}\n","import { Box, CircularProgress } from '@mui/material'\nimport type { MouseEvent, ReactNode } from 'react'\nimport type { DashboardRow, FilterValue } from '../types'\nimport { useDashboard } from './DashboardProvider'\n\n/**\n * Peças comuns dos gráficos ligados ao modelo: paleta, filtro por clique, legenda e os estados de\n * carregando/erro/vazio.\n */\n\n/**\n * Paleta categórica, sempre nesta ordem (a série N tem sempre a mesma cor). Laranja Rocha primeiro;\n * a ordem passa no validador de daltonismo (CVD ΔE ≥ 9 entre vizinhas) e de visão normal (≥ 19).\n */\nexport const SERIES_COLORS = [\n '#F58220',\n '#2a78d6',\n '#1baf7a',\n '#eda100',\n '#e87ba4',\n '#008300',\n '#6250d6',\n '#e34948'\n] as const\n\n/** \"Outros\" (fatias agrupadas) fica neutro, fora da paleta. */\nexport const OTHER_COLOR = '#9e9e9e'\n\nexport const seriesColor = (index: number) => SERIES_COLORS[index % SERIES_COLORS.length] as string\n\n/** Valor numérico de uma coluna da linha (`null` quando vazio). */\nexport const numberOf = (row: DashboardRow, key?: string) =>\n key && row[key] !== null && row[key] !== undefined ? Number(row[key]) : null\n\n/**\n * Filtro cruzado a partir do gráfico: clicar num valor filtra o dashboard por ele (Ctrl/Cmd+clique\n * acumula; clicar de novo no único selecionado limpa). O gráfico deve ignorar o próprio filtro na\n * consulta (`ignoreFilters`) e só destacar a seleção.\n */\nexport function useClickFilter(dimension: string | undefined, enabled: boolean) {\n const { filters, setFilter } = useDashboard()\n const selected = enabled && dimension ? (filters[dimension] ?? []) : []\n\n const toggle = (value: FilterValue, event?: Pick<MouseEvent, 'ctrlKey' | 'metaKey'>) => {\n if (!enabled || !dimension) return\n if (event?.ctrlKey || event?.metaKey)\n setFilter(\n dimension,\n selected.includes(value) ? selected.filter((item) => item !== value) : [...selected, value]\n )\n else setFilter(dimension, selected.length === 1 && selected[0] === value ? [] : [value])\n }\n\n return {\n selected,\n /** Há seleção e este valor não está nela. */\n isDimmed: (value: FilterValue) => selected.length > 0 && !selected.includes(value),\n toggle,\n clear: () => {\n if (enabled && dimension && selected.length) setFilter(dimension, [])\n }\n }\n}\n\nexport function Legend({\n items,\n loading\n}: {\n items: { label: string; color: string }[]\n loading?: boolean\n}) {\n return (\n <Box\n sx={{\n display: 'flex',\n flexWrap: 'wrap',\n columnGap: 1.5,\n rowGap: 0.5,\n mb: 1,\n fontSize: 14,\n color: '#555',\n minHeight: 21\n }}\n >\n {items.map((item) => (\n <Box key={item.label} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>\n <Box sx={{ width: 9, height: 9, borderRadius: '50%', bgcolor: item.color }} />\n {item.label}\n </Box>\n ))}\n {loading && <CircularProgress size={14} sx={{ ml: 'auto' }} />}\n </Box>\n )\n}\n\n/** Erro / carregando / sem dados no lugar do gráfico; `null` quando há o que desenhar (função, não componente). */\nexport function chartPlaceholder({\n height,\n error,\n isLoading,\n empty\n}: {\n height: number\n error: unknown\n isLoading: boolean\n empty: boolean\n}): ReactNode {\n if (!error && !empty) return null\n return (\n <Box\n sx={{\n height,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n color: error ? 'error.main' : '#999'\n }}\n >\n {error\n ? 'Erro ao carregar o gráfico'\n : isLoading\n ? 'Carregando…'\n : 'Sem dados para os filtros selecionados'}\n </Box>\n )\n}\n\n/** `caption`: o que é o título (ex.: o rótulo da dimensão), em cinza acima dele. */\nexport function TooltipBox({\n title,\n caption,\n children\n}: {\n title: string\n caption?: string\n children: ReactNode\n}) {\n return (\n <Box sx={{ bgcolor: '#fff', border: '1px solid #ccc', p: 1, fontSize: 13 }}>\n {caption && <Box sx={{ fontSize: 11, color: '#777' }}>{caption}</Box>}\n <Box sx={{ fontWeight: 700 }}>{title}</Box>\n {children}\n </Box>\n )\n}\n","import type { ChartNumberFormat } from '../layout'\nimport type { ModelMeta } from '../model'\n\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/** `compact`: notação curta do pt-BR (1,2 mil / 3,4 mi / 5,6 bi). */\nexport function formatNumber(\n value: number | string | null | undefined,\n decimals = 2,\n compact = false\n) {\n if (value === null || value === undefined || value === '') return ''\n const number = Number(value)\n if (Number.isNaN(number)) return String(value)\n const key = `${decimals}:${compact}`\n let formatter = formatters.get(key)\n if (!formatter) {\n formatter = new Intl.NumberFormat('pt-BR', {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n ...(compact && { notation: 'compact' })\n })\n formatters.set(key, formatter)\n }\n return formatter.format(number)\n}\n\nexport const formatCurrency = (\n value: number | string | null | undefined,\n decimals = 2,\n compact = false\n) => {\n const text = formatNumber(value, decimals, compact)\n return text && `R$ ${text}`\n}\n\n/**\n * Formata pelo dicionário do modelo (`format`/`decimals` da medida) ou pelo formato do gráfico.\n * `fallbackDecimals` é o padrão do componente quando nenhum dos dois define `decimals`.\n */\nexport function formatMeasure(\n value: number | string | null | undefined,\n meta: ChartNumberFormat | undefined,\n fallbackDecimals = 2\n) {\n if (value === null || value === undefined || value === '') return ''\n const decimals = meta?.decimals ?? (meta?.format === 'integer' ? 0 : fallbackDecimals)\n const compact = meta?.compact ?? false\n let text: string\n switch (meta?.format) {\n case 'currency':\n text = formatCurrency(value, decimals, compact)\n break\n case 'percent': {\n const number = formatNumber(Number(value) * 100, decimals, compact)\n text = number && `${number}%`\n break\n }\n default:\n text = formatNumber(value, decimals, compact)\n }\n return text && `${meta?.prefix ?? ''}${text}${meta?.suffix ?? ''}`\n}\n\n/** Dicionário do modelo com o formato do gráfico por cima. */\nexport function measureFormat(\n meta: ModelMeta | null,\n measure: string,\n formats?: Record<string, ChartNumberFormat>\n): ChartNumberFormat | undefined {\n const fromModel = meta?.measures[measure]\n const fromChart = formats?.[measure]\n if (!fromChart) return fromModel\n return {\n ...(fromModel?.format !== undefined && { format: fromModel.format }),\n ...(fromModel?.decimals !== undefined && { decimals: fromModel.decimals }),\n ...fromChart\n }\n}\n\n/**\n * Formatter de uma medida: o `formats` do gráfico (se houver) por cima do dicionário do modelo; sem\n * `format`, número com `fallbackDecimals`.\n */\nexport function measureFormatter(\n meta: ModelMeta | null,\n measure: string,\n fallbackDecimals = 2,\n formats?: Record<string, ChartNumberFormat>\n): (value: number | string | null | undefined) => string {\n const format = measureFormat(meta, measure, formats)\n return (value) => formatMeasure(value, format, fallbackDecimals)\n}\n\n/** Rótulo de uma dimensão/medida: o `labels` do gráfico, o dicionário do modelo ou o nome. */\nexport function fieldLabel(\n meta: ModelMeta | null,\n name: string,\n labels?: Record<string, string>\n): string {\n return labels?.[name] || meta?.dimensions[name]?.label || meta?.measures[name]?.label || name\n}\n","import { useEffect, useId, useMemo, useState } from 'react'\nimport type { DashboardFilters, DashboardRow } from '../types'\nimport { useDashboard } from './DashboardProvider'\n\nexport const INSPECT_ATTRIBUTE = 'data-bm-dashboard-query'\n\ntype UseModelQueryParams = {\n dimensions?: string[]\n measures?: string[]\n /**\n * Filtros do dashboard que esta consulta NÃO deve respeitar. Um filtro ignora a si mesmo para\n * listar as opções (senão, ao escolher um navio, a lista mostraria só ele).\n */\n ignoreFilters?: string[]\n /** Nome da consulta no devtools (padrão: dimensões × medidas). */\n label?: string\n /** Não consulta (devolve `rows: []`); ex.: a linha de total de uma tabela que não mostra total. */\n skip?: boolean\n}\n\nexport function useModelQuery({\n dimensions,\n measures,\n ignoreFilters,\n label,\n skip = false\n}: UseModelQueryParams) {\n const { model, filters, fetcher, refreshKey, devtools } = useDashboard()\n const instanceId = useId()\n\n const applied: DashboardFilters = {}\n for (const [dimension, values] of Object.entries(filters)) {\n if (values.length && !ignoreFilters?.includes(dimension)) applied[dimension] = values\n }\n\n // Chave por valor: só refaz a consulta quando o que vai pro servidor muda (não a cada render,\n // nem quando muda um filtro que esta consulta ignora)\n const key = JSON.stringify([model, dimensions, measures, applied])\n const query = useMemo(() => ({ model, dimensions, measures, filters: applied }), [key])\n\n const [rows, setRows] = useState<DashboardRow[]>([])\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<unknown>()\n\n const store = devtools?.store\n const debugLabel =\n label ?? [dimensions?.join(', '), measures?.join(', ')].filter(Boolean).join(' × ')\n\n useEffect(() => {\n if (skip) {\n setRows([])\n setIsLoading(false)\n return\n }\n let cancelado = false\n setIsLoading(true)\n const runId = store?.start(instanceId, debugLabel, query)\n fetcher(query)\n .then((result) => {\n if (cancelado) return\n setRows(result.rows)\n setError(undefined)\n if (runId) store?.succeed(runId, result.rows, result.devtools?.debug, result.batched)\n })\n .catch((err) => {\n if (cancelado) return\n setError(err)\n if (runId) store?.fail(runId, err)\n })\n .finally(() => !cancelado && setIsLoading(false))\n return () => {\n cancelado = true\n if (runId) store?.cancel(runId)\n }\n }, [fetcher, query, refreshKey, skip])\n\n useEffect(() => (store ? () => store.remove(instanceId) : undefined), [store, instanceId, skip])\n\n // Espalhar no elemento raiz do componente: é como o inspetor do devtools acha o componente na tela\n const inspectProps = store && !skip ? { [INSPECT_ATTRIBUTE]: instanceId } : {}\n\n return { rows, isLoading, error, inspectProps }\n}\n","import {\n Box,\n Table,\n TableBody,\n TableCell,\n TableContainer,\n TableHead,\n TableRow,\n Typography\n} from '@mui/material'\nimport { AGGREGATION_LABEL, isAggregate, type ModelMeta, type ValueFormat } from '../model'\nimport { useDashboard } from './DashboardProvider'\n\nconst FORMAT_LABEL: Record<ValueFormat, string> = {\n number: 'Número',\n integer: 'Inteiro',\n currency: 'Moeda (R$)',\n percent: 'Percentual'\n}\n\n/** Como a medida é calculada, em termos de negócio (o SQL não sai do servidor). */\nfunction formula(meta: ModelMeta, name: string) {\n const measure = meta.measures[name]\n const label = (ref: string) => meta.measures[ref]?.label ?? ref\n if (measure.type === 'formula' && measure.refs?.length)\n return `Combina ${measure.refs.map(label).join(', ')}`\n if (isAggregate(measure)) return AGGREGATION_LABEL[measure.type]\n if (measure.type !== 'ratio' || !measure.numerator || !measure.denominator) return '—'\n return `${label(measure.numerator)} ÷ ${label(measure.denominator)}`\n}\n\n/**\n * Dicionário do modelo do `DashboardProvider`: o que cada filtro/eixo e cada número significa. É\n * montado a partir de `label`/`description` das dimensões e medidas — documentação que não\n * desatualiza, porque é a mesma definição que calcula os números.\n */\nexport function ModelDictionary() {\n const { meta } = useDashboard()\n if (!meta) return null\n\n const header = (labels: string[]) => (\n <TableHead>\n <TableRow>\n {labels.map((label) => (\n <TableCell key={label} sx={{ fontWeight: 700 }}>\n {label}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n )\n\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Box>\n <Typography sx={{ fontWeight: 700, mb: 1 }}>Indicadores</Typography>\n <TableContainer>\n <Table size=\"small\">\n {header(['Indicador', 'Descrição', 'Cálculo', 'Formato'])}\n <TableBody>\n {Object.entries(meta.measures).map(([name, measure]) => (\n <TableRow key={name}>\n <TableCell sx={{ whiteSpace: 'nowrap' }}>{measure.label ?? name}</TableCell>\n <TableCell>{measure.description ?? '—'}</TableCell>\n <TableCell sx={{ whiteSpace: 'nowrap' }}>{formula(meta, name)}</TableCell>\n <TableCell sx={{ whiteSpace: 'nowrap' }}>\n {measure.format ? FORMAT_LABEL[measure.format] : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n </Box>\n <Box>\n <Typography sx={{ fontWeight: 700, mb: 1 }}>Filtros e agrupamentos</Typography>\n <TableContainer>\n <Table size=\"small\">\n {header(['Campo', 'Descrição'])}\n <TableBody>\n {Object.entries(meta.dimensions).map(([name, dimension]) => (\n <TableRow key={name}>\n <TableCell sx={{ whiteSpace: 'nowrap' }}>{dimension.label ?? name}</TableCell>\n <TableCell>{dimension.description ?? '—'}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </TableContainer>\n </Box>\n </Box>\n )\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as toModelCode, n as literal } from "./codegen-DkfI6UJr.js";
2
+ import { a as toModelCode, n as literal } from "./codegen-LoQzFAle.js";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, join, relative, sep } from "node:path";
5
5
  import { parseArgs } from "node:util";
@@ -167,20 +167,122 @@ function pageFileCode(options) {
167
167
  * `custo`). Sem a opção (ou sem config), `tsx`.
168
168
  */
169
169
  function pageExtension(nextConfig) {
170
- return [...(/pageExtensions\s*:\s*\[([^\]]*)\]/.exec(nextConfig ?? "")?.[1] ?? "").matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]).find((extension) => extension?.endsWith("tsx")) ?? "tsx";
170
+ return pageExtensions(nextConfig).find((extension) => extension.endsWith("tsx")) ?? "tsx";
171
+ }
172
+ /** Itens do `pageExtensions` do `next.config` (lido como texto). */
173
+ function pageExtensions(nextConfig) {
174
+ return [...(/pageExtensions\s*:\s*\[([^\]]*)\]/.exec(nextConfig ?? "")?.[1] ?? "").matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
175
+ }
176
+ /** Valores trocados num template de endpoint do app (`api.template`). */
177
+ const apiTemplateValues = (options) => ({
178
+ __MODELS__: options.modelsImport,
179
+ __MODELS_DIR__: options.modelsDir,
180
+ __LAYOUTS_DIR__: options.layoutsDir,
181
+ __SNAPSHOTS_DIR__: options.snapshotsDir
182
+ });
183
+ function fillApiTemplate(template, options) {
184
+ let content = template;
185
+ for (const [marker, value] of Object.entries(apiTemplateValues(options))) content = content.replaceAll(marker, value);
186
+ return content;
187
+ }
188
+ /** `src/dashboard/models` → `'src', 'dashboard', 'models'` (argumentos do `path.join`). */
189
+ const pathArgs = (dir) => dir.split(/[\\/]/).filter(Boolean).map((part) => literal(part)).join(", ");
190
+ /**
191
+ * Endpoint padrão do `runQuery` com Prisma: um só para todos os dashboards (só aceita os modelos do
192
+ * `models/index.ts`), com o devtools ligado só em desenvolvimento.
193
+ */
194
+ function apiFileCode(options) {
195
+ const { modelsImport, chartTypesImport, appRouter } = options;
196
+ const dir = (path) => `path.join(process.cwd(), ${pathArgs(path)})`;
197
+ const run = (body, indent) => [
198
+ "const result = await runQuery(",
199
+ ` dashboardModels,`,
200
+ ` ${body} as DashboardQuery,`,
201
+ " (sql, params) => prisma.$queryRawUnsafe<unknown[]>(sql, ...params),",
202
+ " { cacheTtlMs: CACHE_TTL_MS, devtools: DEVTOOLS }",
203
+ ")"
204
+ ].map((line) => `${indent}${line}`);
205
+ return [
206
+ "import path from 'node:path'",
207
+ "import { DashboardQueryError, runQuery } from '@bluemarble/bi/server'",
208
+ "import type { DashboardQuery } from '@bluemarble/bi/types'",
209
+ "import { PrismaClient } from '@prisma/client'",
210
+ ...appRouter ? [] : ["import type { NextApiRequest, NextApiResponse } from 'next'"],
211
+ ...chartTypesImport ? [`import { chartTypes } from '${chartTypesImport}'`] : [],
212
+ `import { dashboardModels } from '${modelsImport}'`,
213
+ "",
214
+ "// Client próprio: rotas que chamam `prisma.$disconnect()` no client global derrubariam",
215
+ "// consultas longas em andamento (\"Engine is not yet connected\").",
216
+ "const globalForPrisma = globalThis as unknown as { dashboardPrisma?: PrismaClient }",
217
+ "const prisma = globalForPrisma.dashboardPrisma ?? new PrismaClient()",
218
+ "if (process.env.NODE_ENV === 'development') globalForPrisma.dashboardPrisma = prisma",
219
+ "",
220
+ "const CACHE_TTL_MS = 5 * 60 * 1000",
221
+ "",
222
+ "// Devtools (debug, rascunho de modelo, salvar modelo/layout) aceita SQL vindo do browser: só em dev",
223
+ "const DEVTOOLS = process.env.NODE_ENV === 'development' && {",
224
+ ` modelsDir: ${dir(options.modelsDir)},`,
225
+ ` snapshotsDir: ${dir(options.snapshotsDir)},`,
226
+ ` layoutsDir: ${dir(options.layoutsDir)}${chartTypesImport ? "," : ""}`,
227
+ ...chartTypesImport ? [" chartTypes"] : [],
228
+ "}",
229
+ "",
230
+ "/**",
231
+ " * Endpoint genérico do `@bluemarble/bi`: recebe `{ model, dimensions, measures, filters }` e",
232
+ " * devolve as linhas agregadas. Só aceita nomes definidos nos modelos registrados.",
233
+ " */",
234
+ ...appRouter ? [
235
+ "export async function POST(request: Request) {",
236
+ " try {",
237
+ ...run("(await request.json())", " "),
238
+ " return Response.json(result)",
239
+ " } catch (error) {",
240
+ " if (error instanceof DashboardQueryError)",
241
+ " return Response.json({ error: error.message }, { status: 400 })",
242
+ " throw error",
243
+ " }",
244
+ "}"
245
+ ] : [
246
+ "export default async function handler(req: NextApiRequest, res: NextApiResponse) {",
247
+ " if (req.method !== 'POST') return res.status(405).setHeader('Allow', 'POST').end()",
248
+ " try {",
249
+ ...run("req.body", " "),
250
+ " return res.json(result)",
251
+ " } catch (error) {",
252
+ " if (error instanceof DashboardQueryError)",
253
+ " return res.status(400).json({ error: error.message })",
254
+ " throw error",
255
+ " }",
256
+ "}"
257
+ ],
258
+ ""
259
+ ].join("\n");
260
+ }
261
+ /**
262
+ * Extensão das rotas de API do Pages Router pelo `pageExtensions` do `next.config` (ex.: `api.ts`
263
+ * no `custo`). Sem a opção (ou sem config), `ts`.
264
+ */
265
+ function apiExtension(nextConfig) {
266
+ return pageExtensions(nextConfig).find((extension) => extension.endsWith("ts")) ?? "ts";
171
267
  }
172
268
  //#endregion
173
269
  //#region src/cli/index.ts
174
270
  const HELP = `Uso: bm-bi new <nome> [opções]
175
271
 
176
272
  Cria um dashboard vazio: o modelo (models/<nome>.ts, registrado em models/index.ts), o layout
177
- (layouts/<id>.json) e a página. Depois é montar tudo pelas abas Modelo e Layout do devtools.
273
+ (layouts/<id>.json), a página e, se ainda não existir, o endpoint do runQuery (um só para todos os
274
+ dashboards). Depois é montar tudo pelas abas Modelo e Layout do devtools.
178
275
 
179
276
  A página vai para o router do Next que existir: src/pages (ou pages) →
180
277
  dashboard/<id-em-kebab>/index.<ext>, com a extensão do pageExtensions do next.config; src/app (ou
181
278
  app) → dashboard/<id-em-kebab>/page.tsx, com 'use client'. Com <dir>/page.template, a página sai
182
279
  dele, trocando __MODEL__, __LAYOUT__ (import do JSON), __TITLE__, __COMPONENT__ e __ENDPOINT__.
183
280
 
281
+ O endpoint vai para a rota do --endpoint no mesmo router: src/pages → api/dashboard/query.<ext> (ext
282
+ de API do pageExtensions); src/app → api/dashboard/query/route.ts. O padrão usa Prisma
283
+ ($queryRawUnsafe); com <dir>/api.template, sai dele, trocando __MODELS__ (import do models/index),
284
+ __MODELS_DIR__, __LAYOUTS_DIR__ e __SNAPSHOTS_DIR__ (pastas relativas à raiz do app).
285
+
184
286
  Opções:
185
287
  --id <id> id do layout (padrão: o nome do modelo)
186
288
  --title <texto> título da página (padrão: o id em palavras)
@@ -190,7 +292,11 @@ Opções:
190
292
  --page <arquivo> arquivo da página (padrão: pelo router do Next, ver acima)
191
293
  --skip-page não cria a página
192
294
  --template <arq.> template da página (padrão: <dir>/page.template, se existir)
193
- --endpoint <url> endpoint do runQuery na página padrão (padrão: /api/dashboard/query)
295
+ --endpoint <url> endpoint do runQuery (padrão: /api/dashboard/query)
296
+ --api <arquivo> arquivo do endpoint (padrão: pela rota do --endpoint, ver acima)
297
+ --skip-api não cria o endpoint
298
+ --api-template <a.> template do endpoint (padrão: <dir>/api.template, se existir)
299
+ --snapshots <pasta> pasta dos snapshots no endpoint (padrão: tests/dashboard/snapshots)
194
300
  -h, --help esta ajuda
195
301
  `;
196
302
  const exists = (path) => existsSync(path);
@@ -199,8 +305,8 @@ function importPath(from, to) {
199
305
  const path = relative(dirname(from), to).split(sep).join("/");
200
306
  return path.startsWith(".") ? path : `./${path}`;
201
307
  }
202
- /** Página pelo router do Next que existir no app (Pages primeiro, como o `custo`). */
203
- function defaultPagePath(cwd, slug) {
308
+ /** Router do Next que existir no app (Pages primeiro, como o `custo`). */
309
+ function nextRouter(cwd) {
204
310
  for (const pages of ["src/pages", "pages"]) {
205
311
  if (!exists(join(cwd, pages))) continue;
206
312
  const config = [
@@ -209,15 +315,39 @@ function defaultPagePath(cwd, slug) {
209
315
  "next.config.ts",
210
316
  "next.config.cjs"
211
317
  ].map((file) => join(cwd, file)).find(exists);
212
- const extension = pageExtension(config && readFileSync(config, "utf8"));
213
- return join(cwd, pages, "dashboard", slug, `index.${extension}`);
318
+ return {
319
+ app: false,
320
+ dir: join(cwd, pages),
321
+ config: config && readFileSync(config, "utf8")
322
+ };
214
323
  }
215
- for (const app of ["src/app", "app"]) if (exists(join(cwd, app))) return join(cwd, app, "dashboard", slug, "page.tsx");
324
+ for (const app of ["src/app", "app"]) if (exists(join(cwd, app))) return {
325
+ app: true,
326
+ dir: join(cwd, app),
327
+ config: void 0
328
+ };
329
+ }
330
+ function defaultPagePath(router, slug) {
331
+ if (!router) return void 0;
332
+ return router.app ? join(router.dir, "dashboard", slug, "page.tsx") : join(router.dir, "dashboard", slug, `index.${pageExtension(router.config)}`);
333
+ }
334
+ /** Endpoint na rota do `--endpoint` (ex.: `/api/dashboard/query`). */
335
+ function defaultApiPath(router, endpoint) {
336
+ if (!router) return void 0;
337
+ if (!/^(\/[\w-]+)+$/.test(endpoint)) throw new ScaffoldError(`Endpoint inválido para criar a rota: ${endpoint} (use --api ou --skip-api)`);
338
+ const route = endpoint.slice(1).split("/");
339
+ return router.app ? join(router.dir, ...route, "route.ts") : join(router.dir, ...route.slice(0, -1), `${route.at(-1)}.${apiExtension(router.config)}`);
216
340
  }
217
341
  /** `export const charts` em <dir>/charts: os tipos de gráfico do app entram no provider. */
218
342
  function chartsIndex(dir) {
219
343
  return ["index.ts", "index.tsx"].map((file) => join(dir, "charts", file)).find((path) => exists(path) && /export\s+const\s+charts\b/.test(readFileSync(path, "utf8")));
220
344
  }
345
+ /** `export const chartTypes` em <dir>/charts/types.ts: o devtools do endpoint valida o layout. */
346
+ function chartTypesFile(dir) {
347
+ return ["types.ts", "types.tsx"].map((file) => join(dir, "charts", file)).find((path) => exists(path) && /export\s+const\s+chartTypes\b/.test(readFileSync(path, "utf8")));
348
+ }
349
+ /** Caminho relativo à raiz do app, sempre com `/`. */
350
+ const fromRoot = (cwd, path) => relative(cwd, path).split(sep).join("/");
221
351
  function main(argv) {
222
352
  const { values, positionals } = parseArgs({
223
353
  args: argv,
@@ -238,6 +368,13 @@ function main(argv) {
238
368
  type: "string",
239
369
  default: "/api/dashboard/query"
240
370
  },
371
+ api: { type: "string" },
372
+ "skip-api": { type: "boolean" },
373
+ "api-template": { type: "string" },
374
+ snapshots: {
375
+ type: "string",
376
+ default: "tests/dashboard/snapshots"
377
+ },
241
378
  help: {
242
379
  type: "boolean",
243
380
  short: "h"
@@ -262,9 +399,13 @@ function main(argv) {
262
399
  const indexPath = join(modelsDir, "index.ts");
263
400
  const layoutPath = join(layoutsDir, `${id}.json`);
264
401
  const dashboardDir = join(cwd, values.dir);
265
- const pagePath = values["skip-page"] ? void 0 : values.page ? join(cwd, values.page) : defaultPagePath(cwd, kebabCase(id));
402
+ const router = nextRouter(cwd);
403
+ const pagePath = values["skip-page"] ? void 0 : values.page ? join(cwd, values.page) : defaultPagePath(router, kebabCase(id));
404
+ const apiPath = values["skip-api"] ? void 0 : values.api ? join(cwd, values.api) : defaultApiPath(router, values.endpoint);
405
+ const apiExists = !!apiPath && exists(apiPath);
266
406
  const templatePath = values.template ? join(cwd, values.template) : [join(dashboardDir, "page.template")].find(exists);
267
- if (templatePath && !exists(templatePath)) throw new ScaffoldError(`Template não encontrado: ${relative(cwd, templatePath)}`);
407
+ const apiTemplatePath = values["api-template"] ? join(cwd, values["api-template"]) : [join(dashboardDir, "api.template")].find(exists);
408
+ for (const path of [templatePath, apiTemplatePath]) if (path && !exists(path)) throw new ScaffoldError(`Template não encontrado: ${relative(cwd, path)}`);
268
409
  for (const path of [
269
410
  modelPath,
270
411
  layoutPath,
@@ -284,6 +425,19 @@ function main(argv) {
284
425
  };
285
426
  page = templatePath ? fillPageTemplate(readFileSync(templatePath, "utf8"), options) : pageFileCode(options);
286
427
  }
428
+ let api;
429
+ if (apiPath && !apiExists) {
430
+ const chartTypes = chartTypesFile(dashboardDir);
431
+ const options = {
432
+ modelsImport: importPath(apiPath, modelsDir),
433
+ modelsDir: fromRoot(cwd, modelsDir),
434
+ layoutsDir: fromRoot(cwd, layoutsDir),
435
+ snapshotsDir: fromRoot(cwd, join(cwd, values.snapshots)),
436
+ chartTypesImport: chartTypes && importPath(apiPath, chartTypes.replace(/\.tsx?$/, "")),
437
+ appRouter: basename(apiPath).startsWith("route.")
438
+ };
439
+ api = apiTemplatePath ? fillApiTemplate(readFileSync(apiTemplatePath, "utf8"), options) : apiFileCode(options);
440
+ }
287
441
  const written = [];
288
442
  const write = (path, content) => {
289
443
  mkdirSync(dirname(path), { recursive: true });
@@ -294,6 +448,7 @@ function main(argv) {
294
448
  if (index !== null) write(indexPath, index);
295
449
  write(layoutPath, layoutFileCode(id));
296
450
  if (pagePath && page !== void 0) write(pagePath, page);
451
+ if (apiPath && api !== void 0) write(apiPath, api);
297
452
  process.stdout.write([
298
453
  ...written.map((path) => `✓ ${path}`),
299
454
  ...pagePath ? templatePath ? [` (página do template ${relative(cwd, templatePath)})`] : [] : values["skip-page"] ? [] : [
@@ -304,6 +459,7 @@ function main(argv) {
304
459
  " <DashboardCharts layout={layout} />",
305
460
  " </DashboardProvider>"
306
461
  ],
462
+ ...apiPath ? apiExists ? [`· ${relative(cwd, apiPath)} (endpoint já existe, mantido)`] : apiTemplatePath ? [` (endpoint do template ${relative(cwd, apiTemplatePath)})`] : [] : values["skip-api"] ? [] : ["", "Endpoint não criado: não achei src/pages nem src/app. Use --api <arquivo>."],
307
463
  ""
308
464
  ].join("\n"));
309
465
  }