@bluemarble/bi 0.6.2 → 0.6.3

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.
@@ -115,6 +115,58 @@ function createLayoutStore() {
115
115
  };
116
116
  }
117
117
  //#endregion
118
+ //#region src/client/batchQueries.ts
119
+ /**
120
+ * Manda num request só (`{ model, queries }`) as consultas de um modelo feitas no mesmo ciclo — as
121
+ * de um render da página. O servidor junta numa SQL só as que dão para juntar (mesmas dimensões,
122
+ * filtros e fatos; ver `runBatch`): os KPIs viram uma consulta com todas as medidas, e "Vol. Total"
123
+ * (fórmula de "Vol. Convencional" + "Vol. Esteira") reaproveita as agregações dos outros dois.
124
+ *
125
+ * Consultas do devtools (`query.devtools`) e de metadados passam direto. Se o lote falhar (ou o
126
+ * servidor for de uma versão sem lote), cada consulta é refeita sozinha.
127
+ */
128
+ function batchQueries(fetcher) {
129
+ const batches = /* @__PURE__ */ new Map();
130
+ const alone = ({ query, resolve, reject }) => fetcher(query).then(resolve, reject);
131
+ const flush = (model) => {
132
+ const members = batches.get(model) ?? [];
133
+ batches.delete(model);
134
+ if (members.length === 1) return alone(members[0]);
135
+ fetcher({
136
+ model,
137
+ queries: members.map(({ query: { dimensions, measures, filters } }) => ({
138
+ dimensions,
139
+ measures,
140
+ filters
141
+ }))
142
+ }).then(({ results }) => {
143
+ if (results?.length !== members.length) return members.forEach(alone);
144
+ members.forEach(({ resolve, reject }, index) => {
145
+ const { error, ...result } = results[index];
146
+ if (error) reject(new Error(error));
147
+ else resolve(result);
148
+ });
149
+ }, () => members.forEach(alone));
150
+ };
151
+ return (query) => {
152
+ if (query.meta || query.devtools || query.queries) return fetcher(query);
153
+ return new Promise((resolve, reject) => {
154
+ const members = batches.get(query.model);
155
+ if (members) return void members.push({
156
+ query,
157
+ resolve,
158
+ reject
159
+ });
160
+ batches.set(query.model, [{
161
+ query,
162
+ resolve,
163
+ reject
164
+ }]);
165
+ setTimeout(() => flush(query.model), 0);
166
+ });
167
+ };
168
+ }
169
+ //#endregion
118
170
  //#region src/devtools/store.ts
119
171
  const MAX_HISTORY = 100;
120
172
  /** `STORM_RUNS` execuções da mesma instância dentro de `STORM_WINDOW_MS` = consulta em loop. */
@@ -193,10 +245,11 @@ function createDevtoolsStore() {
193
245
  emit();
194
246
  return run.runId;
195
247
  },
196
- succeed: (runId, rows, debug) => update(runId, {
248
+ succeed: (runId, rows, debug, batched) => update(runId, {
197
249
  status: "success",
198
250
  rows,
199
- debug
251
+ debug,
252
+ batched
200
253
  }),
201
254
  fail: (runId, error) => update(runId, {
202
255
  status: "error",
@@ -230,7 +283,7 @@ function createDevtoolsStore() {
230
283
  //#endregion
231
284
  //#region src/client/DashboardProvider.tsx
232
285
  const DashboardContext = createContext(null);
233
- const DashboardDevtools = lazy(() => import("./DashboardDevtools-BT9mJrmJ.js"));
286
+ const DashboardDevtools = lazy(() => import("./DashboardDevtools-Osu5V30U.js"));
234
287
  const DEVTOOLS_DEFAULT = process.env.NODE_ENV === "development";
235
288
  const draftStorageKey = (model) => `bm-dashboard-devtools:draft:${model}`;
236
289
  function loadDraft(model) {
@@ -245,7 +298,7 @@ function loadDraft(model) {
245
298
  * Guarda os filtros do dashboard. Todo componente dentro dele (filtros, gráficos, tabelas) consulta o
246
299
  * mesmo modelo com os mesmos filtros — é o que faz um slicer filtrar a página inteira, como no BI.
247
300
  */
248
- function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEVTOOLS_DEFAULT, charts, children }) {
301
+ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEVTOOLS_DEFAULT, charts, batch = true, children }) {
249
302
  const chartTypes = useMemo(() => resolveChartTypes(charts), [charts]);
250
303
  const chartComponents = useMemo(() => Object.fromEntries(Object.entries(charts ?? {}).map(([name, spec]) => [name, spec.component])), [charts]);
251
304
  const [filters, setFilters] = useState(defaultFilters);
@@ -287,6 +340,7 @@ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEV
287
340
  devtools,
288
341
  draft
289
342
  ]);
343
+ const queryFetcher = useMemo(() => batch ? batchQueries(effectiveFetcher) : effectiveFetcher, [batch, effectiveFetcher]);
290
344
  const rawFetcher = useMemo(() => (query) => fetcher({
291
345
  ...query,
292
346
  devtools: {
@@ -326,7 +380,7 @@ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEV
326
380
  model,
327
381
  filters,
328
382
  setFilter,
329
- fetcher: effectiveFetcher,
383
+ fetcher: queryFetcher,
330
384
  meta,
331
385
  refreshKey,
332
386
  chartTypes,
@@ -336,7 +390,7 @@ function DashboardProvider({ model, fetcher, defaultFilters = {}, devtools = DEV
336
390
  model,
337
391
  filters,
338
392
  setFilter,
339
- effectiveFetcher,
393
+ queryFetcher,
340
394
  meta,
341
395
  refreshKey,
342
396
  chartTypes,
@@ -463,7 +517,7 @@ function useModelQuery({ dimensions, measures, ignoreFilters, label, skip = fals
463
517
  if (cancelado) return;
464
518
  setRows(result.rows);
465
519
  setError(void 0);
466
- if (runId) store?.succeed(runId, result.rows, result.devtools?.debug);
520
+ if (runId) store?.succeed(runId, result.rows, result.devtools?.debug, result.batched);
467
521
  }).catch((err) => {
468
522
  if (cancelado) return;
469
523
  setError(err);
@@ -572,4 +626,4 @@ function ModelDictionary() {
572
626
  //#endregion
573
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 };
574
628
 
575
- //# sourceMappingURL=ModelDictionary-BlDeDyAE.js.map
629
+ //# sourceMappingURL=ModelDictionary-Dc3_LKqr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelDictionary-Dc3_LKqr.js","names":["loadDraft"],"sources":["../src/devtools/layoutStore.ts","../src/client/batchQueries.ts","../src/devtools/store.ts","../src/client/DashboardProvider.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 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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"}
@@ -1,4 +1,4 @@
1
- import { A as DashboardRow, C as ColumnVerticalAlign, D as DashboardQuery, E as DashboardFilters$1, K as ModelDef, N as FilterValue, O as DashboardQueryDebug, S as ColumnAlign, d as ChartsLayout, i as CardStyle, k as DashboardResult, l as ChartTypeSpec, o as ChartDef, q as ModelMeta, s as ChartNumberFormat, u as ChartTypes, w as FormatRule } from "../layout-B7eYgNyR.js";
1
+ import { A as DashboardQueryDebug, C as ColumnVerticalAlign, F as FilterValue, J as ModelDef, M as DashboardRow, O as DashboardFilters$1, S as ColumnAlign, Y as ModelMeta, d as ChartsLayout, i as CardStyle, j as DashboardResult, k as DashboardQuery, l as ChartTypeSpec, o as ChartDef, s as ChartNumberFormat, u as ChartTypes, w as FormatRule } from "../layout-Bn1OiRov.js";
2
2
  import { ComponentType, MouseEvent, ReactNode } from "react";
3
3
  //#region src/client/BarChart.d.ts
4
4
  type BarChartProps = {
@@ -249,6 +249,8 @@ type QueryRun = {
249
249
  rows?: DashboardRow[];
250
250
  error?: string;
251
251
  debug?: DashboardQueryDebug;
252
+ /** Foi num request junto com outras consultas (quantas, contando ela); o `debug` é do conjunto. */
253
+ batched?: number;
252
254
  };
253
255
  type TrackedInstance = {
254
256
  instanceId: string;
@@ -267,7 +269,7 @@ declare function createDevtoolsStore(): {
267
269
  subscribe(listener: () => void): () => boolean;
268
270
  getSnapshot: () => DevtoolsSnapshot;
269
271
  start(instanceId: string, label: string, query: DashboardQuery): number;
270
- succeed: (runId: number, rows: DashboardRow[], debug?: DashboardQueryDebug) => void;
272
+ succeed: (runId: number, rows: DashboardRow[], debug?: DashboardQueryDebug, batched?: number) => void;
271
273
  fail: (runId: number, error: unknown) => void;
272
274
  /** Consulta descartada antes de responder (filtro mudou, componente desmontou). */
273
275
  cancel: (runId: number) => void;
@@ -342,13 +344,18 @@ type DashboardProviderProps = {
342
344
  * `devtools.chartTypes` para salvar o layout.
343
345
  */
344
346
  charts?: Record<string, ChartComponentSpec>;
347
+ /**
348
+ * Manda as consultas de um render num request só; o servidor junta numa SQL as que têm as mesmas
349
+ * dimensões, filtros e fatos (ex.: os KPIs). Padrão: ligado.
350
+ */
351
+ batch?: boolean;
345
352
  children: ReactNode;
346
353
  };
347
354
  /**
348
355
  * Guarda os filtros do dashboard. Todo componente dentro dele (filtros, gráficos, tabelas) consulta o
349
356
  * mesmo modelo com os mesmos filtros — é o que faz um slicer filtrar a página inteira, como no BI.
350
357
  */
351
- declare function DashboardProvider({ model, fetcher, defaultFilters, devtools, charts, children }: DashboardProviderProps): import("react").JSX.Element;
358
+ declare function DashboardProvider({ model, fetcher, defaultFilters, devtools, charts, batch, children }: DashboardProviderProps): import("react").JSX.Element;
352
359
  declare function useDashboard(): DashboardContextValue;
353
360
  //#endregion
354
361
  //#region src/client/DataTable.d.ts
@@ -1,5 +1,5 @@
1
1
  import { T as rowRuleStyles, _ as tableColumns, f as isBuiltinChart, h as percentColumn, o as chartCardStyle, u as customChartProps } from "../layout-DAFeHIuP.js";
2
- import { a as formatCurrency, c as measureFormat, d as useDashboard, i as fieldLabel, l as measureFormatter, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as DashboardProvider } from "../ModelDictionary-BlDeDyAE.js";
2
+ import { a as formatCurrency, c as measureFormat, d as useDashboard, i as fieldLabel, l as measureFormatter, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, r as useModelQuery, s as formatNumber, t as ModelDictionary, u as DashboardProvider } from "../ModelDictionary-Dc3_LKqr.js";
3
3
  import { Box, Checkbox, CircularProgress, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInput, Paper, Select, Table, TableBody, TableCell, TableHead, TableRow, TableSortLabel, Typography } from "@mui/material";
4
4
  import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, ComposedChart, LabelList, Line, Pie, PieChart, ResponsiveContainer, Tooltip as Tooltip$1, XAxis, YAxis } from "recharts";
5
5
  import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
@@ -175,8 +175,27 @@ type DashboardQuery = {
175
175
  filters?: DashboardFilters;
176
176
  /** Devolve só os metadados do modelo (rótulos, descrições, formatos; sem SQL) em `result.meta`. */
177
177
  meta?: boolean;
178
+ /**
179
+ * Várias consultas do mesmo modelo num request (o client manda assim as consultas de um render).
180
+ * O servidor junta numa SQL só as que têm as mesmas dimensões e filtros e vêm das mesmas fatos, e
181
+ * devolve uma resposta por consulta em `result.results`, na mesma ordem. Do `devtools`, só
182
+ * `debug` e `model` (rascunho) valem aqui.
183
+ */
184
+ queries?: DashboardBatchQuery[];
178
185
  devtools?: DashboardDevtoolsRequest;
179
186
  };
187
+ /** Uma consulta de um lote (`DashboardQuery.queries`): o modelo e o devtools são os do lote. */
188
+ type DashboardBatchQuery = Pick<DashboardQuery, 'dimensions' | 'measures' | 'filters'>;
189
+ /** Resposta de uma consulta do lote: as linhas ou o erro (de consulta) dela. */
190
+ type DashboardBatchResult = {
191
+ rows: DashboardRow[];
192
+ error?: string;
193
+ /** Quantas consultas do lote saíram da mesma SQL (contando esta); o `debug` é o dessa SQL. */
194
+ batched?: number;
195
+ devtools?: {
196
+ debug?: DashboardQueryDebug;
197
+ };
198
+ };
180
199
  type DashboardRow = Record<string, string | number | null>;
181
200
  type DashboardQueryDebug = {
182
201
  sql: string;
@@ -243,6 +262,13 @@ type DevtoolsCapabilities = {
243
262
  };
244
263
  type DashboardResult = {
245
264
  rows: DashboardRow[];
265
+ /** Respostas de um lote (`query.queries`), na ordem das consultas. */
266
+ results?: DashboardBatchResult[];
267
+ /**
268
+ * Quantas consultas saíram da mesma SQL que esta (contando ela), quando foi num lote. O
269
+ * `devtools.debug` é o dessa SQL.
270
+ */
271
+ batched?: number;
246
272
  meta?: ModelMeta;
247
273
  devtools?: {
248
274
  model?: ModelDef;
@@ -605,5 +631,5 @@ declare function validateLayout(layout: ChartsLayout, types?: ChartTypes): strin
605
631
  */
606
632
  declare function layoutModelWarnings(layout: ChartsLayout, model: Pick<ModelDef, 'dimensions' | 'measures'>, types?: ChartTypes): string[];
607
633
  //#endregion
608
- export { modelMeta as $, DashboardRow as A, Aggregation as B, ColumnVerticalAlign as C, DashboardQuery as D, DashboardFilters as E, ModelValidation as F, MeasureMeta as G, FieldMeta as H, ProfileStep as I, RatioMeasure as J, ModelDef as K, SaveModelResult as L, DevtoolsCapabilities as M, FilterValue as N, DashboardQueryDebug as O, ModelColumns as P, ValueFormat as Q, SchemaTable as R, ColumnAlign as S, DashboardDevtoolsRequest as T, FormulaMeasure as U, DimensionDef as V, MeasureDef as W, SumMeasure as X, RelationshipDef as Y, TableDef as Z, KpiChartDef as _, CardsStyle as a, resolveChartTypes as b, ChartType as c, ChartsLayout as d, validateModel as et, ComboChartDef as f, FilterDef as g, FieldSpec as h, CardStyle as i, DashboardSnapshot as j, DashboardResult as k, ChartTypeSpec as l, DonutChartDef as m, BuiltinChartDef as n, ChartDef as o, CustomChartDef as p, ModelMeta as q, CanvasDef as r, ChartNumberFormat as s, BarChartDef as t, ChartTypes as u, TableChartDef as v, FormatRule as w, validateLayout as x, layoutModelWarnings as y, AggregateMeasure as z };
609
- //# sourceMappingURL=layout-B7eYgNyR.d.ts.map
634
+ export { TableDef as $, DashboardQueryDebug as A, SchemaTable as B, ColumnVerticalAlign as C, DashboardDevtoolsRequest as D, DashboardBatchResult as E, FilterValue as F, FormulaMeasure as G, Aggregation as H, ModelColumns as I, ModelDef as J, MeasureDef as K, ModelValidation as L, DashboardRow as M, DashboardSnapshot as N, DashboardFilters as O, DevtoolsCapabilities as P, SumMeasure as Q, ProfileStep as R, ColumnAlign as S, DashboardBatchQuery as T, DimensionDef as U, AggregateMeasure as V, FieldMeta as W, RatioMeasure as X, ModelMeta as Y, RelationshipDef as Z, KpiChartDef as _, CardsStyle as a, resolveChartTypes as b, ChartType as c, ChartsLayout as d, ValueFormat as et, ComboChartDef as f, FilterDef as g, FieldSpec as h, CardStyle as i, DashboardResult as j, DashboardQuery as k, ChartTypeSpec as l, DonutChartDef as m, BuiltinChartDef as n, validateModel as nt, ChartDef as o, CustomChartDef as p, MeasureMeta as q, CanvasDef as r, ChartNumberFormat as s, BarChartDef as t, modelMeta as tt, ChartTypes as u, TableChartDef as v, FormatRule as w, validateLayout as x, layoutModelWarnings as y, SaveModelResult as z };
635
+ //# sourceMappingURL=layout-Bn1OiRov.d.ts.map
@@ -1,4 +1,4 @@
1
- import { $ as modelMeta, A as DashboardRow, B as Aggregation, D as DashboardQuery, G as MeasureMeta, H as FieldMeta, J as RatioMeasure, K as ModelDef, Q as ValueFormat, U as FormulaMeasure, V as DimensionDef, W as MeasureDef, X as SumMeasure, Y as RelationshipDef, Z as TableDef, b as resolveChartTypes, d as ChartsLayout, et as validateModel, k as DashboardResult, l as ChartTypeSpec, x as validateLayout, y as layoutModelWarnings, z as AggregateMeasure } from "../layout-B7eYgNyR.js";
1
+ import { $ as TableDef, G as FormulaMeasure, H as Aggregation, J as ModelDef, K as MeasureDef, M as DashboardRow, Q as SumMeasure, U as DimensionDef, V as AggregateMeasure, W as FieldMeta, X as RatioMeasure, Z as RelationshipDef, b as resolveChartTypes, d as ChartsLayout, et as ValueFormat, j as DashboardResult, k as DashboardQuery, l as ChartTypeSpec, nt as validateModel, q as MeasureMeta, tt as modelMeta, x as validateLayout, y as layoutModelWarnings } from "../layout-Bn1OiRov.js";
2
2
  //#region src/codegen.d.ts
3
3
  /** JSON com as chaves ordenadas: compara modelos sem depender da ordem das propriedades. */
4
4
  declare function canonicalJson(value: unknown): string;
@@ -28,6 +28,26 @@ function leafMeasures(model, names) {
28
28
  });
29
29
  return [...leaves];
30
30
  }
31
+ /**
32
+ * De onde saem as medidas: cada tabela onde agregam (a fato) e as tabelas que as agregações dela
33
+ * citam. Duas consultas com as mesmas dimensões, filtros e origens dão as mesmas linhas, e cada
34
+ * medida o mesmo valor, juntas ou separadas — é o que o lote do `runQuery` usa para juntá-las.
35
+ */
36
+ function measureSources(model, measures) {
37
+ const tableNames = Object.keys(model.tables);
38
+ const edges = modelEdges(model);
39
+ const sources = {};
40
+ for (const leaf of leafMeasures(model, measures)) {
41
+ const measure = model.measures[leaf];
42
+ if (!isAggregate(measure)) continue;
43
+ const tables = referencedTables(measure.sql, tableNames);
44
+ const home = measureHome(tables, edges);
45
+ if (!home) throw new DashboardQueryError(`Medida "${leaf}" sem tabela de origem`);
46
+ sources[home] ??= /* @__PURE__ */ new Set();
47
+ for (const table of tables) sources[home].add(table);
48
+ }
49
+ return Object.fromEntries(Object.entries(sources).sort(([a], [b]) => a.localeCompare(b)).map(([home, tables]) => [home, [...tables].sort()]));
50
+ }
31
51
  function compileFilters(model, query) {
32
52
  const filters = [];
33
53
  for (const [name, values] of Object.entries(query.filters ?? {})) {
@@ -231,6 +251,114 @@ function defineModel(model) {
231
251
  return model;
232
252
  }
233
253
  //#endregion
254
+ //#region src/server/batch.ts
255
+ const cleanFilters = (filters = {}) => Object.fromEntries(Object.keys(filters).filter((name) => Array.isArray(filters[name]) && filters[name].length).sort().map((name) => [name, filters[name]]));
256
+ /**
257
+ * Pode juntar? Com dimensões, as origens têm que ser as mesmas: uma fato a mais traria grupos que só
258
+ * existem nela. Sem dimensões (totais) sai sempre uma linha, então basta que cada fato em comum
259
+ * agregue com as mesmas tabelas (uma tabela a mais no JOIN de uma fato poderia mudar o valor dela).
260
+ */
261
+ function compatible(a, b, totals) {
262
+ if (!totals) return JSON.stringify(a) === JSON.stringify(b);
263
+ return Object.keys(a).every((home) => !b[home] || a[home].join() === b[home].join());
264
+ }
265
+ /**
266
+ * Agrupa as consultas do lote: mesmas dimensões e filtros e origens compatíveis viram uma consulta
267
+ * com todas as medidas. Consulta só de dimensões (opções de filtro) só junta com outra igual.
268
+ * Erro de uma consulta (medida desconhecida) fica em `errors`, sem derrubar as outras.
269
+ */
270
+ function planBatch(model, queries) {
271
+ const candidates = [];
272
+ const errors = /* @__PURE__ */ new Map();
273
+ queries.forEach((raw, index) => {
274
+ const query = {
275
+ dimensions: raw?.dimensions ?? [],
276
+ measures: [...new Set(raw?.measures ?? [])],
277
+ filters: cleanFilters(raw?.filters)
278
+ };
279
+ const key = JSON.stringify([
280
+ query.dimensions,
281
+ query.filters,
282
+ !query.measures.length
283
+ ]);
284
+ let sources = null;
285
+ try {
286
+ if (query.measures.length) sources = measureSources(model, query.measures);
287
+ } catch (error) {
288
+ if (!(error instanceof DashboardQueryError)) throw error;
289
+ errors.set(index, error.message);
290
+ return;
291
+ }
292
+ const totals = !query.dimensions.length;
293
+ const group = candidates.find((candidate) => candidate.key === key && (sources && candidate.sources ? compatible(candidate.sources, sources, totals) : JSON.stringify(candidate.query.measures) === JSON.stringify(query.measures)));
294
+ if (!group) {
295
+ candidates.push({
296
+ key,
297
+ sources,
298
+ query,
299
+ members: [index]
300
+ });
301
+ return;
302
+ }
303
+ group.members.push(index);
304
+ group.query.measures = [.../* @__PURE__ */ new Set([...group.query.measures, ...query.measures])];
305
+ if (group.sources && sources) group.sources = {
306
+ ...group.sources,
307
+ ...sources
308
+ };
309
+ });
310
+ return {
311
+ groups: candidates.map(({ query, members }) => ({
312
+ query: {
313
+ ...query,
314
+ measures: [...query.measures].sort()
315
+ },
316
+ members
317
+ })),
318
+ errors
319
+ };
320
+ }
321
+ /**
322
+ * Executa o lote: uma consulta por grupo (em paralelo) e as linhas de cada consulta só com as
323
+ * colunas que ela pediu. `toQueryError` diz se um erro do grupo é da consulta (vai para as
324
+ * respostas dela) ou do servidor (`null`: derruba o request, como numa consulta sozinha).
325
+ */
326
+ async function runBatch(model, queries, run, options) {
327
+ if (!Array.isArray(queries)) throw new DashboardQueryError("`queries` deve ser uma lista");
328
+ const { groups, errors } = planBatch(model, queries);
329
+ const results = queries.map((_, index) => {
330
+ const error = errors.get(index);
331
+ return error ? {
332
+ rows: [],
333
+ error
334
+ } : { rows: [] };
335
+ });
336
+ await Promise.all(groups.map(async ({ query, members }) => {
337
+ let execution;
338
+ try {
339
+ execution = await run(query);
340
+ } catch (error) {
341
+ const queryError = options.toQueryError(error);
342
+ if (!queryError) throw error;
343
+ for (const index of members) results[index] = {
344
+ rows: [],
345
+ error: queryError.message
346
+ };
347
+ return;
348
+ }
349
+ for (const index of members) {
350
+ const own = queries[index];
351
+ const columns = [...own.dimensions ?? [], ...own.measures ?? []];
352
+ results[index] = {
353
+ rows: execution.rows.map((row) => Object.fromEntries(columns.map((column) => [column, row[column] ?? null]))),
354
+ ...members.length > 1 && { batched: members.length },
355
+ ...options.debug && { devtools: { debug: execution.debug } }
356
+ };
357
+ }
358
+ }));
359
+ return results;
360
+ }
361
+ //#endregion
234
362
  //#region src/server/modelFile.ts
235
363
  /**
236
364
  * Troca, dentro do arquivo do modelo, só o objeto passado ao `defineModel(...)` e o import do
@@ -715,8 +843,11 @@ async function runQuery(models, query, execute, options = {}) {
715
843
  meta: modelMeta(model)
716
844
  };
717
845
  const ttl = options.cacheTtlMs ?? 0;
718
- const run = async (target) => {
719
- const { sql, params } = compileQuery(target, query);
846
+ const run = async (target, compiled = query) => {
847
+ const { sql, params } = compileQuery(target, {
848
+ model: query.model,
849
+ ...compiled
850
+ });
720
851
  const load = async () => {
721
852
  const startedAt = performance.now();
722
853
  const rows = await execute(sql, params);
@@ -736,6 +867,33 @@ async function runQuery(models, query, execute, options = {}) {
736
867
  ...await execution
737
868
  };
738
869
  };
870
+ const toQueryError = (error) => {
871
+ if (error instanceof DashboardQueryError) return error;
872
+ return devtools ? new DashboardQueryError(dbMessage(error)) : null;
873
+ };
874
+ if (query.queries) {
875
+ const { debug: _, model: __, ...others } = devtools ?? {};
876
+ if (Object.values(others).some(Boolean)) throw new DashboardQueryError("Num lote, o devtools só aceita `debug` e `model`");
877
+ return {
878
+ rows: [],
879
+ results: await runBatch(model, query.queries, async (compiled) => {
880
+ const { rows, sql, params, durationMs, cached } = await run(model, compiled);
881
+ return {
882
+ rows,
883
+ debug: {
884
+ sql,
885
+ params,
886
+ durationMs,
887
+ cached,
888
+ draft: !!draft
889
+ }
890
+ };
891
+ }, {
892
+ debug: !!devtools?.debug,
893
+ toQueryError
894
+ })
895
+ };
896
+ }
739
897
  if (devtools) {
740
898
  if (devtools.clearCache) cache.clear();
741
899
  try {
@@ -756,8 +914,7 @@ async function runQuery(models, query, execute, options = {}) {
756
914
  }
757
915
  }
758
916
  const { rows, sql, params, durationMs, cached } = await run(model).catch((error) => {
759
- if (!devtools || error instanceof DashboardQueryError) throw error;
760
- throw new DashboardQueryError(dbMessage(error));
917
+ throw toQueryError(error) ?? error;
761
918
  });
762
919
  if (!devtools?.debug) return { rows };
763
920
  return {