@bluemarble/bi 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"codegen-LoQzFAle.js","names":[],"sources":["../src/star.ts","../src/model.ts","../src/codegen.ts"],"sourcesContent":["/**\n * Modelo estrela (`tables` + `relationships`): tabelas fato e dimensão ligadas por relacionamentos\n * N:1, como no modelo do Power BI. Aqui ficam as regras do grafo — quem alcança quem e por qual\n * caminho —, usadas pela validação, pelo compilador e pelo diagrama do devtools.\n *\n * O filtro \"desce\" do lado 1 para o lado N: uma medida da fato `fPesagem` enxerga a dimensão\n * `dOperacao` se existe um caminho `fPesagem -> ... -> dOperacao` seguindo os relacionamentos no\n * sentido N -> 1 (snowflake vale: `fPesagem -> dOperacao -> dNavio`).\n */\n\nimport type { ModelDef, RelationshipDef } from './model'\n\nexport type ColumnRef = { table: string; column: string }\n\nexport type Edge = { from: ColumnRef; to: ColumnRef; index: number; inner: boolean }\n\n/** `'tabela.coluna'` -> `{ table, column }` (nulo se não tiver esse formato). */\nexport function parseColumnRef(ref: unknown): ColumnRef | null {\n if (typeof ref !== 'string') return null\n const match = /^([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(ref.trim())\n return match ? { table: match[1], column: match[2] } : null\n}\n\n/** Relacionamentos válidos do modelo, como arestas N -> 1. */\nexport function modelEdges(model: Pick<ModelDef, 'relationships'>): Edge[] {\n return (model.relationships ?? []).flatMap((relationship: RelationshipDef, index) => {\n const from = parseColumnRef(relationship?.from)\n const to = parseColumnRef(relationship?.to)\n return from && to ? [{ from, to, index, inner: relationship.inner === true }] : []\n })\n}\n\nconst escapeRegex = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/**\n * Tabelas citadas no SQL como `tabela.coluna`. O que está entre aspas simples é ignorado, para\n * `'a.b'` num literal não contar como referência.\n */\nexport function referencedTables(sql: string, tables: string[]): string[] {\n const code = sql.replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n return tables.filter((name) => new RegExp(`(?<![\\\\w.\\`])${escapeRegex(name)}\\\\.`).test(code))\n}\n\n/** Passo de um caminho: entra em `table` pela aresta `edge` (de `edge.from.table`). */\nexport type PathStep = { table: string; edge: Edge }\n\nexport type Reach = {\n /** Tabela -> caminho mais curto a partir da origem (vazio para a própria origem). */\n paths: Map<string, PathStep[]>\n /**\n * Tabelas alcançadas por mais de um caminho (de qualquer tamanho): o filtro ficaria ambíguo. É a\n * mesma regra do Power BI, que só deixa um caminho ativo entre duas tabelas.\n */\n ambiguous: string[]\n}\n\n/** Busca em largura a partir de `start`, seguindo os relacionamentos no sentido N -> 1. */\nexport function reachFrom(start: string, edges: Edge[]): Reach {\n const paths = new Map<string, PathStep[]>([[start, []]])\n let frontier = [start]\n while (frontier.length) {\n const next: string[] = []\n for (const table of frontier) {\n for (const edge of edges) {\n if (edge.from.table !== table || paths.has(edge.to.table)) continue\n paths.set(edge.to.table, [...(paths.get(table) ?? []), { table: edge.to.table, edge }])\n next.push(edge.to.table)\n }\n }\n frontier = next\n }\n // Cada aresta saindo de uma tabela alcançada é um caminho até o destino dela\n const incoming = new Map<string, number>()\n for (const edge of edges)\n if (paths.has(edge.from.table))\n incoming.set(edge.to.table, (incoming.get(edge.to.table) ?? 0) + 1)\n const ambiguous = [...incoming]\n .filter(([table, count]) => table !== start && count > 1)\n .map(([table]) => table)\n return { paths, ambiguous }\n}\n\n/**\n * Tabelas que precisam entrar na consulta, na ordem do JOIN, para que a partir de `home` se chegue\n * a todas as `targets`. Como cada tabela é alcançada por um caminho só (o modelo valida ambiguidade),\n * a união dos caminhos é uma árvore: cada tabela entra uma vez, ligada pela aresta de quem veio antes.\n */\nexport function joinPlan(home: string, targets: Iterable<string>, edges: Edge[]): PathStep[] {\n const { paths } = reachFrom(home, edges)\n const plan: PathStep[] = []\n const added = new Set([home])\n const include = (target: string) => {\n for (const step of paths.get(target) ?? []) {\n if (added.has(step.table)) continue\n added.add(step.table)\n plan.push(step)\n }\n }\n for (const target of targets) include(target)\n // Relacionamento `inner` entra sempre que o lado N está na consulta: ele filtra mesmo sem\n // dimensão pedida (repete até não entrar mais nada, porque cada tabela nova pode ter os seus)\n for (let grew = true; grew;) {\n grew = false\n for (const edge of edges)\n if (edge.inner && added.has(edge.from.table) && !added.has(edge.to.table)) {\n include(edge.to.table)\n grew = true\n }\n }\n // Pai antes do filho: ordena pela profundidade do caminho\n const depth = (table: string) => paths.get(table)?.length ?? 0\n return plan.sort((a, b) => depth(a.table) - depth(b.table))\n}\n\n/**\n * Tabela de origem de uma medida `sum`: entre as tabelas que o SQL cita, a que alcança todas as\n * outras (ex.: `fPesagem.peso * dProduto.fator` agrega em `fPesagem`). Nulo se nenhuma alcança.\n */\nexport function measureHome(tables: string[], edges: Edge[]): string | null {\n if (!tables.length) return null\n return (\n tables.find((candidate) => {\n const { paths } = reachFrom(candidate, edges)\n return tables.every((table) => paths.has(table))\n }) ?? null\n )\n}\n","import { measureHome, modelEdges, parseColumnRef, reachFrom, referencedTables } from './star'\n\n/**\n * Tipos e validação do modelo semântico. Fica fora de `server/` porque o devtools (client) também\n * valida o rascunho editado antes de mandá-lo pro servidor.\n */\n\n/**\n * Dicionário: nome de negócio e explicação de cada campo. Os componentes usam como padrão (rótulo,\n * formatação) e o `ModelDictionary` vira a documentação do modelo para quem usa o dashboard.\n */\nexport type FieldMeta = {\n label?: string\n description?: string\n}\n\nexport const VALUE_FORMATS = ['number', 'integer', 'currency', 'percent'] as const\nexport type ValueFormat = (typeof VALUE_FORMATS)[number]\n\nexport type MeasureMeta = FieldMeta & {\n /** `percent` espera a fração (0.25 = 25%). */\n format?: ValueFormat\n /** Casas decimais; sem isso, vale o padrão do componente. */\n decimals?: number\n}\n\nexport type DimensionDef = FieldMeta & {\n sql: string\n type: 'number' | 'string'\n}\n\n/**\n * Agregações sobre uma expressão SQL (`tabela.coluna`). Todas valem nos dois formatos; no estrela,\n * agregam no grão da tabela de origem, como a soma.\n */\nexport const AGGREGATIONS = ['sum', 'count', 'countDistinct', 'avg', 'min', 'max'] as const\nexport type Aggregation = (typeof AGGREGATIONS)[number]\n\nexport const AGGREGATION_LABEL: Record<Aggregation, string> = {\n sum: 'Soma',\n count: 'Contagem',\n countDistinct: 'Contagem distinta',\n avg: 'Média',\n min: 'Mínimo',\n max: 'Máximo'\n}\n\n/** `COUNT` conta os valores não nulos; para contar linhas, use uma coluna que nunca é nula. */\nexport type AggregateMeasure = MeasureMeta & { type: Aggregation; sql: string }\nexport type SumMeasure = AggregateMeasure & { type: 'sum' }\nexport type RatioMeasure = MeasureMeta & { type: 'ratio'; numerator: string; denominator: string }\n/**\n * Combinação de outras medidas, já agregadas: `{nome}` vira o valor da medida (como `[Medida]` no\n * DAX). É o jeito de juntar medidas de fatos diferentes: `COALESCE({custoMmo}, 0) + COALESCE({custoAppa}, 0)`.\n */\nexport type FormulaMeasure = MeasureMeta & { type: 'formula'; expression: string }\nexport type MeasureDef = AggregateMeasure | RatioMeasure | FormulaMeasure\n\nexport const isAggregate = (measure: { type: unknown }): measure is AggregateMeasure =>\n AGGREGATIONS.includes(measure.type as Aggregation)\n\n/** O SQL agregado da medida: `SUM(x)`, `COUNT(DISTINCT x)`, `AVG(x)`... */\nexport function aggregateSql(measure: Pick<AggregateMeasure, 'type' | 'sql'>): string {\n if (measure.type === 'countDistinct') return `COUNT(DISTINCT ${measure.sql})`\n const fn = { sum: 'SUM', count: 'COUNT', avg: 'AVG', min: 'MIN', max: 'MAX' }[measure.type]\n return `${fn}(${measure.sql})`\n}\n\n/**\n * Tabela do modelo: o SQL é a \"consulta de origem\" (tabela, view ou SELECT). Não há tipo fato ou\n * dimensão: o papel sai dos relacionamentos (quem está no lado 1 filtra quem está no lado N) e de\n * onde ficam as medidas.\n */\nexport type TableDef = FieldMeta & {\n sql: string\n /** Coluna única da tabela; o diagrama usa ela para decidir o lado 1 ao criar relacionamentos. */\n key?: string\n}\n\n/** Relacionamento N:1 — o filtro do lado 1 (`to`) chega às linhas do lado N (`from`). */\nexport type RelationshipDef = {\n /** `tabela.coluna` do lado N (os lançamentos). */\n from: string\n /** `tabela.coluna` do lado 1 (a chave do cadastro). */\n to: string\n /**\n * \"Pressupor integridade referencial\" do Power BI: INNER JOIN em vez de LEFT JOIN, e a tabela do\n * lado 1 entra sempre na consulta da fato. Linhas do lado N sem par no lado 1 ficam de fora (em\n * vez de virar um grupo \"em branco\") — é o jeito de um filtro fixo da dimensão valer para a fato.\n */\n inner?: boolean\n}\n\n/**\n * Modelo estrela: `tables` (fato/dimensão) + `relationships`. Dimensões e medidas citam as colunas\n * como `tabela.coluna`; cada medida de agregação agrega no grão da própria tabela e chega às\n * dimensões seguindo os relacionamentos.\n */\nexport type ModelDef = {\n name: string\n tables: Record<string, TableDef>\n relationships?: RelationshipDef[]\n dimensions: Record<string, DimensionDef>\n measures: Record<string, MeasureDef>\n /**\n * Posição de cada card no diagrama do devtools (tabela -> canto superior esquerdo) e, se foi\n * redimensionado, o tamanho. Só visual: não muda nenhuma consulta. Card sem posição entra no\n * layout automático; sem tamanho, tem a largura padrão e a altura do conteúdo.\n */\n diagram?: Record<string, DiagramPosition>\n}\n\nexport type DiagramPosition = { x: number; y: number; width?: number; height?: number }\n\n/** Nomes citados como `{nome}` numa fórmula. */\nexport const formulaRefs = (expression: string) => [\n ...new Set([...expression.matchAll(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g)].map((match) => match[1]))\n]\n\n/** Medidas que uma medida usa diretamente (razão e fórmula). */\nexport function measureDeps(measure: MeasureDef): string[] {\n if (measure.type === 'ratio') return [measure.numerator, measure.denominator]\n if (measure.type === 'formula') return formulaRefs(measure.expression)\n return []\n}\n\nconst IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst isText = (value: unknown): value is string => typeof value === 'string' && !!value.trim()\n\nfunction metaErrors(context: string, field: Record<string, unknown>): string[] {\n const errors: string[] = []\n for (const key of ['label', 'description'] as const) {\n if (field[key] !== undefined && typeof field[key] !== 'string')\n errors.push(`${context}: \\`${key}\\` precisa ser texto`)\n }\n if (field.format !== undefined && !VALUE_FORMATS.includes(field.format as ValueFormat))\n errors.push(`${context}: formato inválido \"${field.format}\"`)\n const { decimals } = field\n if (\n decimals !== undefined &&\n (typeof decimals !== 'number' || !Number.isInteger(decimals) || decimals < 0 || decimals > 10)\n )\n errors.push(`${context}: \\`decimals\\` precisa ser um inteiro entre 0 e 10`)\n return errors\n}\n\n/** Só os metadados (sem SQL): é o que o client recebe fora do devtools. */\nexport type ModelMeta = {\n name: string\n /** `table`: tabela do modelo de onde a dimensão sai (o nome, sem o SQL). */\n dimensions: Record<string, FieldMeta & { type: DimensionDef['type']; table?: string }>\n measures: Record<\n string,\n MeasureMeta & {\n type: MeasureDef['type']\n numerator?: string\n denominator?: string\n /** Medidas que a fórmula combina (a expressão em si fica no servidor). */\n refs?: string[]\n }\n >\n}\n\nexport function modelMeta(model: ModelDef): ModelMeta {\n const tableNames = Object.keys(model.tables)\n const pick = <T extends FieldMeta & MeasureMeta>(field: T) => ({\n ...(field.label !== undefined && { label: field.label }),\n ...(field.description !== undefined && { description: field.description }),\n ...(field.format !== undefined && { format: field.format }),\n ...(field.decimals !== undefined && { decimals: field.decimals })\n })\n return {\n name: model.name,\n dimensions: Object.fromEntries(\n Object.entries(model.dimensions).map(([name, dim]) => [\n name,\n { type: dim.type, table: referencedTables(dim.sql, tableNames)[0], ...pick(dim) }\n ])\n ),\n measures: Object.fromEntries(\n Object.entries(model.measures).map(([name, measure]) => [\n name,\n {\n type: measure.type,\n // Contagem sem formato: inteiro (sem casas decimais)\n ...((measure.type === 'count' || measure.type === 'countDistinct') && {\n format: 'integer' as const\n }),\n ...pick(measure),\n ...(measure.type === 'ratio' && {\n numerator: measure.numerator,\n denominator: measure.denominator\n }),\n ...(measure.type === 'formula' && { refs: formulaRefs(measure.expression) })\n }\n ])\n )\n }\n}\n\n/**\n * Lista os problemas do modelo (vazio = válido). Confere também a forma do objeto, porque o rascunho\n * do devtools chega como JSON da request.\n */\nexport function validateModel(model: ModelDef): string[] {\n if (!isRecord(model)) return ['modelo inválido']\n if (!isRecord(model.dimensions) || !isRecord(model.measures))\n return ['`dimensions` e `measures` são obrigatórios']\n // Rascunho/arquivo antigo: o formato base + joins foi removido\n if ('base' in model || 'joins' in model)\n return ['o formato `base` + `joins` não existe mais: use `tables` + `relationships`']\n if (!isRecord(model.tables)) return ['`tables` é obrigatório']\n if (model.relationships !== undefined && !Array.isArray(model.relationships))\n return ['`relationships` precisa ser uma lista']\n\n if (model.diagram !== undefined && !diagramValid(model.diagram))\n return ['`diagram` precisa ser { nome: { x, y, width?, height? } } com números (tamanho > 0)']\n\n const errors: string[] = []\n const { tables } = model\n const names = [...Object.keys(model.dimensions), ...Object.keys(model.measures)]\n\n for (const name of [...names, ...Object.keys(tables)]) {\n if (!IDENTIFIER.test(name)) errors.push(`nome inválido: \"${name}\"`)\n else if (name.startsWith('__')) errors.push(`nome reservado: \"${name}\" (começa com \"__\")`)\n }\n\n const duplicated = names.find((name, index) => names.indexOf(name) !== index)\n if (duplicated) errors.push(`nome repetido: \"${duplicated}\"`)\n\n for (const [name, table] of Object.entries(tables)) {\n if (!isRecord(table) || !isText(table.sql)) {\n errors.push(`tabela \"${name}\" sem SQL`)\n continue\n }\n if (table.key !== undefined && (typeof table.key !== 'string' || !IDENTIFIER.test(table.key)))\n errors.push(`tabela \"${name}\": key inválida`)\n errors.push(...metaErrors(`tabela \"${name}\"`, table))\n }\n\n const tableNames = Object.keys(tables)\n errors.push(...relationshipErrors(model, tableNames))\n\n for (const [name, dimension] of Object.entries(model.dimensions)) {\n if (!isRecord(dimension) || !isText(dimension.sql)) errors.push(`dimensão \"${name}\" sem SQL`)\n else if (dimension.type !== 'number' && dimension.type !== 'string')\n errors.push(`dimensão \"${name}\" com tipo inválido`)\n else {\n errors.push(...metaErrors(`dimensão \"${name}\"`, dimension))\n if (referencedTables(dimension.sql, tableNames).length !== 1)\n errors.push(`dimensão \"${name}\" precisa usar colunas de uma tabela só (\\`tabela.coluna\\`)`)\n }\n }\n\n const edges = modelEdges(model)\n for (const [name, measure] of Object.entries(model.measures)) {\n if (!isRecord(measure)) {\n errors.push(`medida \"${name}\" inválida`)\n } else if (isAggregate(measure)) {\n if (!isText(measure.sql)) errors.push(`medida \"${name}\" sem SQL`)\n else {\n const used = referencedTables(measure.sql, tableNames)\n if (!used.length)\n errors.push(`medida \"${name}\" precisa usar colunas de uma tabela (\\`tabela.coluna\\`)`)\n else if (!measureHome(used, edges))\n errors.push(\n `medida \"${name}\" usa ${used.join(', ')}, mas nenhuma delas chega às outras pelos relacionamentos`\n )\n }\n } else if (measure.type === 'ratio' || measure.type === 'formula') {\n if (measure.type === 'formula' && !isText(measure.expression))\n errors.push(`medida \"${name}\" sem expressão`)\n else if (measure.type === 'formula' && !formulaRefs(measure.expression).length)\n errors.push(`medida \"${name}\": a fórmula precisa citar medidas como {nome}`)\n for (const ref of measureDeps(measure)) {\n if (typeof ref !== 'string' || !model.measures[ref])\n errors.push(`medida \"${name}\" referencia \"${ref}\", que não existe`)\n }\n } else {\n errors.push(`medida \"${name}\" com tipo inválido`)\n }\n if (isRecord(measure)) errors.push(...metaErrors(`medida \"${name}\"`, measure))\n }\n if (errors.length) return errors\n\n // Medidas circulares (a -> b -> a) só estourariam na hora de compilar a consulta\n const visit = (name: string, stack: string[]): string | null => {\n if (stack.includes(name)) return [...stack, name].join(' -> ')\n for (const dep of measureDeps(model.measures[name])) {\n const cycle = visit(dep, [...stack, name])\n if (cycle) return cycle\n }\n return null\n }\n for (const name of Object.keys(model.measures)) {\n const cycle = visit(name, [])\n if (cycle) {\n errors.push(`medida circular: ${cycle}`)\n break\n }\n }\n\n return errors\n}\n\nconst diagramValid = (diagram: unknown) =>\n isRecord(diagram) &&\n Object.values(diagram).every(\n (point) =>\n isRecord(point) &&\n Number.isFinite(point.x) &&\n Number.isFinite(point.y) &&\n [point.width, point.height].every(\n (size) => size === undefined || (Number.isFinite(size) && (size as number) > 0)\n )\n )\n\nfunction relationshipErrors(model: ModelDef, tableNames: string[]): string[] {\n const errors: string[] = []\n const seen = new Set<string>()\n for (const [index, relationship] of (model.relationships ?? []).entries()) {\n const context = `relacionamento ${index + 1}`\n if (!isRecord(relationship)) {\n errors.push(`${context} inválido`)\n continue\n }\n const from = parseColumnRef(relationship.from)\n const to = parseColumnRef(relationship.to)\n if (!from || !to) {\n errors.push(`${context}: use \\`tabela.coluna\\` em \\`from\\` e \\`to\\``)\n continue\n }\n for (const ref of [from, to])\n if (!tableNames.includes(ref.table))\n errors.push(`${context}: tabela \"${ref.table}\" não existe`)\n if (from.table === to.table)\n errors.push(`${context}: liga a tabela \"${from.table}\" a ela mesma`)\n if (relationship.inner !== undefined && typeof relationship.inner !== 'boolean')\n errors.push(`${context}: \\`inner\\` precisa ser true/false`)\n const pair = `${from.table}->${to.table}`\n if (seen.has(pair))\n errors.push(`${context}: já existe um relacionamento de \"${from.table}\" para \"${to.table}\"`)\n seen.add(pair)\n }\n if (errors.length) return errors\n\n // Dois caminhos até a mesma tabela deixariam o filtro ambíguo (o Power BI deixa um inativo)\n const edges = modelEdges(model)\n for (const table of tableNames) {\n const { ambiguous, paths } = reachFrom(table, edges)\n if (ambiguous.length) {\n errors.push(\n `caminho ambíguo de \"${table}\" até \"${ambiguous[0]}\": há mais de um relacionamento levando lá`\n )\n break\n }\n // Ciclo: a tabela alcança a si mesma\n if (edges.some((edge) => edge.to.table === table && paths.has(edge.from.table))) {\n errors.push(`relacionamentos em ciclo passando por \"${table}\"`)\n break\n }\n }\n return errors\n}\n","import {\n type DiagramPosition,\n type FieldMeta,\n isAggregate,\n type MeasureDef,\n type MeasureMeta,\n type ModelDef,\n type TableDef\n} from './model'\n\n/**\n * Gera o código TS de um modelo (`defineModel({...})`). Usado pelo \"Copiar TS\" do devtools e pelo\n * \"Salvar no arquivo\" do servidor, que troca só a chamada do `defineModel` dentro do arquivo.\n */\n\nconst diagramCode = ({ x, y, width, height }: DiagramPosition) =>\n `{ x: ${x}, y: ${y}${width === undefined ? '' : `, width: ${width}`}${height === undefined ? '' : `, height: ${height}`} }`\n\n/** JSON com as chaves ordenadas: compara modelos sem depender da ordem das propriedades. */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(value, (_, item) =>\n item && typeof item === 'object' && !Array.isArray(item)\n ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : item\n )\n}\n\nexport function literal(text: string) {\n if (text.includes('\\n'))\n return `\\`${text.replaceAll('\\\\', '\\\\\\\\').replaceAll('`', '\\\\`').replaceAll('${', '\\\\${')}\\``\n return `'${text.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\")}'`\n}\n\nfunction metaEntries(field: FieldMeta & MeasureMeta) {\n const entries: string[] = []\n if (field.label !== undefined) entries.push(`label: ${literal(field.label)}`)\n if (field.description !== undefined) entries.push(`description: ${literal(field.description)}`)\n if (field.format !== undefined) entries.push(`format: '${field.format}'`)\n if (field.decimals !== undefined) entries.push(`decimals: ${field.decimals}`)\n return entries\n}\n\nfunction measureCode(measure: MeasureDef) {\n const meta = metaEntries(measure)\n const args = isAggregate(measure)\n ? [literal(measure.sql)]\n : measure.type === 'formula'\n ? [literal(measure.expression)]\n : [literal(measure.numerator), literal(measure.denominator)]\n if (meta.length) args.push(`{ ${meta.join(', ')} }`)\n return `${measure.type}(${args.join(', ')})`\n}\n\nfunction tableCode(table: TableDef) {\n const fields = [\n ...(table.key !== undefined ? [`key: ${literal(table.key)}`] : []),\n `sql: ${literal(table.sql)}`,\n ...metaEntries(table)\n ]\n return `{ ${fields.join(', ')} }`\n}\n\nconst entries = <T>(record: Record<string, T>, render: (value: T) => string) =>\n Object.entries(record)\n .map(([name, value]) => ` ${name}: ${render(value)}`)\n .join(',\\n')\n\n/** `{ ... }` com uma entrada por linha, ou `{}` quando vazio (modelo recém-criado). */\nconst block = <T>(record: Record<string, T>, render: (value: T) => string) =>\n Object.keys(record).length ? `{\\n${entries(record, render)}\\n }` : '{}'\n\n/** O objeto passado ao `defineModel`, começando em `{` (sem indentação na primeira linha). */\nexport function modelObjectCode(model: ModelDef): string {\n const lines = ['{', ` name: ${literal(model.name)},`]\n lines.push(` tables: ${block(model.tables, tableCode)},`)\n if (model.relationships?.length)\n lines.push(\n ` relationships: [\\n${model.relationships\n .map(\n (relationship) =>\n ` { from: ${literal(relationship.from)}, to: ${literal(relationship.to)}${relationship.inner ? ', inner: true' : ''} }`\n )\n .join(',\\n')}\\n ],`\n )\n lines.push(\n ` dimensions: ${block(model.dimensions, (dim) =>\n [`{ sql: ${literal(dim.sql)}`, `type: '${dim.type}'`, ...metaEntries(dim)]\n .join(', ')\n .concat(' }')\n )},`,\n ` measures: ${block(model.measures, measureCode)}${model.diagram ? ',' : ''}`\n )\n if (model.diagram) lines.push(` diagram: {\\n${entries(model.diagram, diagramCode)}\\n }`)\n lines.push('}')\n return lines.join('\\n')\n}\n\n/** Helpers do `@bluemarble/bi/server` que o código do modelo usa. */\nexport function modelImports(model: ModelDef) {\n // O helper tem o nome do tipo (`sum(...)`, `countDistinct(...)`, `formula(...)`)\n const types = new Set(Object.values(model.measures).map((measure) => measure.type))\n return ['defineModel', ...types].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))\n}\n\n/** Arquivo completo, pronto para colar em `src/dashboard/models/<nome>.ts`. */\nexport function toModelCode(model: ModelDef): string {\n return [\n `import { ${modelImports(model).join(', ')} } from '@bluemarble/bi/server'`,\n '',\n `export const ${model.name} = defineModel(${modelObjectCode(model)})`,\n ''\n ].join('\\n')\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"codegen-LoQzFAle.js","names":[],"sources":["../src/star.ts","../src/model.ts","../src/codegen.ts"],"sourcesContent":["/**\r\n * Modelo estrela (`tables` + `relationships`): tabelas fato e dimensão ligadas por relacionamentos\r\n * N:1, como no modelo do Power BI. Aqui ficam as regras do grafo — quem alcança quem e por qual\r\n * caminho —, usadas pela validação, pelo compilador e pelo diagrama do devtools.\r\n *\r\n * O filtro \"desce\" do lado 1 para o lado N: uma medida da fato `fPesagem` enxerga a dimensão\r\n * `dOperacao` se existe um caminho `fPesagem -> ... -> dOperacao` seguindo os relacionamentos no\r\n * sentido N -> 1 (snowflake vale: `fPesagem -> dOperacao -> dNavio`).\r\n */\r\n\r\nimport type { ModelDef, RelationshipDef } from './model'\r\n\r\nexport type ColumnRef = { table: string; column: string }\r\n\r\nexport type Edge = { from: ColumnRef; to: ColumnRef; index: number; inner: boolean }\r\n\r\n/** `'tabela.coluna'` -> `{ table, column }` (nulo se não tiver esse formato). */\r\nexport function parseColumnRef(ref: unknown): ColumnRef | null {\r\n if (typeof ref !== 'string') return null\r\n const match = /^([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(ref.trim())\r\n return match ? { table: match[1], column: match[2] } : null\r\n}\r\n\r\n/** Relacionamentos válidos do modelo, como arestas N -> 1. */\r\nexport function modelEdges(model: Pick<ModelDef, 'relationships'>): Edge[] {\r\n return (model.relationships ?? []).flatMap((relationship: RelationshipDef, index) => {\r\n const from = parseColumnRef(relationship?.from)\r\n const to = parseColumnRef(relationship?.to)\r\n return from && to ? [{ from, to, index, inner: relationship.inner === true }] : []\r\n })\r\n}\r\n\r\nconst escapeRegex = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\r\n\r\n/**\r\n * Tabelas citadas no SQL como `tabela.coluna`. O que está entre aspas simples é ignorado, para\r\n * `'a.b'` num literal não contar como referência.\r\n */\r\nexport function referencedTables(sql: string, tables: string[]): string[] {\r\n const code = sql.replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\r\n return tables.filter((name) => new RegExp(`(?<![\\\\w.\\`])${escapeRegex(name)}\\\\.`).test(code))\r\n}\r\n\r\n/** Passo de um caminho: entra em `table` pela aresta `edge` (de `edge.from.table`). */\r\nexport type PathStep = { table: string; edge: Edge }\r\n\r\nexport type Reach = {\r\n /** Tabela -> caminho mais curto a partir da origem (vazio para a própria origem). */\r\n paths: Map<string, PathStep[]>\r\n /**\r\n * Tabelas alcançadas por mais de um caminho (de qualquer tamanho): o filtro ficaria ambíguo. É a\r\n * mesma regra do Power BI, que só deixa um caminho ativo entre duas tabelas.\r\n */\r\n ambiguous: string[]\r\n}\r\n\r\n/** Busca em largura a partir de `start`, seguindo os relacionamentos no sentido N -> 1. */\r\nexport function reachFrom(start: string, edges: Edge[]): Reach {\r\n const paths = new Map<string, PathStep[]>([[start, []]])\r\n let frontier = [start]\r\n while (frontier.length) {\r\n const next: string[] = []\r\n for (const table of frontier) {\r\n for (const edge of edges) {\r\n if (edge.from.table !== table || paths.has(edge.to.table)) continue\r\n paths.set(edge.to.table, [...(paths.get(table) ?? []), { table: edge.to.table, edge }])\r\n next.push(edge.to.table)\r\n }\r\n }\r\n frontier = next\r\n }\r\n // Cada aresta saindo de uma tabela alcançada é um caminho até o destino dela\r\n const incoming = new Map<string, number>()\r\n for (const edge of edges)\r\n if (paths.has(edge.from.table))\r\n incoming.set(edge.to.table, (incoming.get(edge.to.table) ?? 0) + 1)\r\n const ambiguous = [...incoming]\r\n .filter(([table, count]) => table !== start && count > 1)\r\n .map(([table]) => table)\r\n return { paths, ambiguous }\r\n}\r\n\r\n/**\r\n * Tabelas que precisam entrar na consulta, na ordem do JOIN, para que a partir de `home` se chegue\r\n * a todas as `targets`. Como cada tabela é alcançada por um caminho só (o modelo valida ambiguidade),\r\n * a união dos caminhos é uma árvore: cada tabela entra uma vez, ligada pela aresta de quem veio antes.\r\n */\r\nexport function joinPlan(home: string, targets: Iterable<string>, edges: Edge[]): PathStep[] {\r\n const { paths } = reachFrom(home, edges)\r\n const plan: PathStep[] = []\r\n const added = new Set([home])\r\n const include = (target: string) => {\r\n for (const step of paths.get(target) ?? []) {\r\n if (added.has(step.table)) continue\r\n added.add(step.table)\r\n plan.push(step)\r\n }\r\n }\r\n for (const target of targets) include(target)\r\n // Relacionamento `inner` entra sempre que o lado N está na consulta: ele filtra mesmo sem\r\n // dimensão pedida (repete até não entrar mais nada, porque cada tabela nova pode ter os seus)\r\n for (let grew = true; grew;) {\r\n grew = false\r\n for (const edge of edges)\r\n if (edge.inner && added.has(edge.from.table) && !added.has(edge.to.table)) {\r\n include(edge.to.table)\r\n grew = true\r\n }\r\n }\r\n // Pai antes do filho: ordena pela profundidade do caminho\r\n const depth = (table: string) => paths.get(table)?.length ?? 0\r\n return plan.sort((a, b) => depth(a.table) - depth(b.table))\r\n}\r\n\r\n/**\r\n * Tabela de origem de uma medida `sum`: entre as tabelas que o SQL cita, a que alcança todas as\r\n * outras (ex.: `fPesagem.peso * dProduto.fator` agrega em `fPesagem`). Nulo se nenhuma alcança.\r\n */\r\nexport function measureHome(tables: string[], edges: Edge[]): string | null {\r\n if (!tables.length) return null\r\n return (\r\n tables.find((candidate) => {\r\n const { paths } = reachFrom(candidate, edges)\r\n return tables.every((table) => paths.has(table))\r\n }) ?? null\r\n )\r\n}\r\n","import { measureHome, modelEdges, parseColumnRef, reachFrom, referencedTables } from './star'\r\n\r\n/**\r\n * Tipos e validação do modelo semântico. Fica fora de `server/` porque o devtools (client) também\r\n * valida o rascunho editado antes de mandá-lo pro servidor.\r\n */\r\n\r\n/**\r\n * Dicionário: nome de negócio e explicação de cada campo. Os componentes usam como padrão (rótulo,\r\n * formatação) e o `ModelDictionary` vira a documentação do modelo para quem usa o dashboard.\r\n */\r\nexport type FieldMeta = {\r\n label?: string\r\n description?: string\r\n}\r\n\r\nexport const VALUE_FORMATS = ['number', 'integer', 'currency', 'percent'] as const\r\nexport type ValueFormat = (typeof VALUE_FORMATS)[number]\r\n\r\nexport type MeasureMeta = FieldMeta & {\r\n /** `percent` espera a fração (0.25 = 25%). */\r\n format?: ValueFormat\r\n /** Casas decimais; sem isso, vale o padrão do componente. */\r\n decimals?: number\r\n}\r\n\r\nexport type DimensionDef = FieldMeta & {\r\n sql: string\r\n type: 'number' | 'string'\r\n}\r\n\r\n/**\r\n * Agregações sobre uma expressão SQL (`tabela.coluna`). Todas valem nos dois formatos; no estrela,\r\n * agregam no grão da tabela de origem, como a soma.\r\n */\r\nexport const AGGREGATIONS = ['sum', 'count', 'countDistinct', 'avg', 'min', 'max'] as const\r\nexport type Aggregation = (typeof AGGREGATIONS)[number]\r\n\r\nexport const AGGREGATION_LABEL: Record<Aggregation, string> = {\r\n sum: 'Soma',\r\n count: 'Contagem',\r\n countDistinct: 'Contagem distinta',\r\n avg: 'Média',\r\n min: 'Mínimo',\r\n max: 'Máximo'\r\n}\r\n\r\n/** `COUNT` conta os valores não nulos; para contar linhas, use uma coluna que nunca é nula. */\r\nexport type AggregateMeasure = MeasureMeta & { type: Aggregation; sql: string }\r\nexport type SumMeasure = AggregateMeasure & { type: 'sum' }\r\nexport type RatioMeasure = MeasureMeta & { type: 'ratio'; numerator: string; denominator: string }\r\n/**\r\n * Combinação de outras medidas, já agregadas: `{nome}` vira o valor da medida (como `[Medida]` no\r\n * DAX). É o jeito de juntar medidas de fatos diferentes: `COALESCE({custoMmo}, 0) + COALESCE({custoAppa}, 0)`.\r\n */\r\nexport type FormulaMeasure = MeasureMeta & { type: 'formula'; expression: string }\r\nexport type MeasureDef = AggregateMeasure | RatioMeasure | FormulaMeasure\r\n\r\nexport const isAggregate = (measure: { type: unknown }): measure is AggregateMeasure =>\r\n AGGREGATIONS.includes(measure.type as Aggregation)\r\n\r\n/** O SQL agregado da medida: `SUM(x)`, `COUNT(DISTINCT x)`, `AVG(x)`... */\r\nexport function aggregateSql(measure: Pick<AggregateMeasure, 'type' | 'sql'>): string {\r\n if (measure.type === 'countDistinct') return `COUNT(DISTINCT ${measure.sql})`\r\n const fn = { sum: 'SUM', count: 'COUNT', avg: 'AVG', min: 'MIN', max: 'MAX' }[measure.type]\r\n return `${fn}(${measure.sql})`\r\n}\r\n\r\n/**\r\n * Tabela do modelo: o SQL é a \"consulta de origem\" (tabela, view ou SELECT). Não há tipo fato ou\r\n * dimensão: o papel sai dos relacionamentos (quem está no lado 1 filtra quem está no lado N) e de\r\n * onde ficam as medidas.\r\n */\r\nexport type TableDef = FieldMeta & {\r\n sql: string\r\n /** Coluna única da tabela; o diagrama usa ela para decidir o lado 1 ao criar relacionamentos. */\r\n key?: string\r\n}\r\n\r\n/** Relacionamento N:1 — o filtro do lado 1 (`to`) chega às linhas do lado N (`from`). */\r\nexport type RelationshipDef = {\r\n /** `tabela.coluna` do lado N (os lançamentos). */\r\n from: string\r\n /** `tabela.coluna` do lado 1 (a chave do cadastro). */\r\n to: string\r\n /**\r\n * \"Pressupor integridade referencial\" do Power BI: INNER JOIN em vez de LEFT JOIN, e a tabela do\r\n * lado 1 entra sempre na consulta da fato. Linhas do lado N sem par no lado 1 ficam de fora (em\r\n * vez de virar um grupo \"em branco\") — é o jeito de um filtro fixo da dimensão valer para a fato.\r\n */\r\n inner?: boolean\r\n}\r\n\r\n/**\r\n * Modelo estrela: `tables` (fato/dimensão) + `relationships`. Dimensões e medidas citam as colunas\r\n * como `tabela.coluna`; cada medida de agregação agrega no grão da própria tabela e chega às\r\n * dimensões seguindo os relacionamentos.\r\n */\r\nexport type ModelDef = {\r\n name: string\r\n tables: Record<string, TableDef>\r\n relationships?: RelationshipDef[]\r\n dimensions: Record<string, DimensionDef>\r\n measures: Record<string, MeasureDef>\r\n /**\r\n * Posição de cada card no diagrama do devtools (tabela -> canto superior esquerdo) e, se foi\r\n * redimensionado, o tamanho. Só visual: não muda nenhuma consulta. Card sem posição entra no\r\n * layout automático; sem tamanho, tem a largura padrão e a altura do conteúdo.\r\n */\r\n diagram?: Record<string, DiagramPosition>\r\n}\r\n\r\nexport type DiagramPosition = { x: number; y: number; width?: number; height?: number }\r\n\r\n/** Nomes citados como `{nome}` numa fórmula. */\r\nexport const formulaRefs = (expression: string) => [\r\n ...new Set([...expression.matchAll(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g)].map((match) => match[1]))\r\n]\r\n\r\n/** Medidas que uma medida usa diretamente (razão e fórmula). */\r\nexport function measureDeps(measure: MeasureDef): string[] {\r\n if (measure.type === 'ratio') return [measure.numerator, measure.denominator]\r\n if (measure.type === 'formula') return formulaRefs(measure.expression)\r\n return []\r\n}\r\n\r\nconst IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/\r\n\r\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\r\n typeof value === 'object' && value !== null && !Array.isArray(value)\r\n\r\nconst isText = (value: unknown): value is string => typeof value === 'string' && !!value.trim()\r\n\r\nfunction metaErrors(context: string, field: Record<string, unknown>): string[] {\r\n const errors: string[] = []\r\n for (const key of ['label', 'description'] as const) {\r\n if (field[key] !== undefined && typeof field[key] !== 'string')\r\n errors.push(`${context}: \\`${key}\\` precisa ser texto`)\r\n }\r\n if (field.format !== undefined && !VALUE_FORMATS.includes(field.format as ValueFormat))\r\n errors.push(`${context}: formato inválido \"${field.format}\"`)\r\n const { decimals } = field\r\n if (\r\n decimals !== undefined &&\r\n (typeof decimals !== 'number' || !Number.isInteger(decimals) || decimals < 0 || decimals > 10)\r\n )\r\n errors.push(`${context}: \\`decimals\\` precisa ser um inteiro entre 0 e 10`)\r\n return errors\r\n}\r\n\r\n/** Só os metadados (sem SQL): é o que o client recebe fora do devtools. */\r\nexport type ModelMeta = {\r\n name: string\r\n /** `table`: tabela do modelo de onde a dimensão sai (o nome, sem o SQL). */\r\n dimensions: Record<string, FieldMeta & { type: DimensionDef['type']; table?: string }>\r\n measures: Record<\r\n string,\r\n MeasureMeta & {\r\n type: MeasureDef['type']\r\n numerator?: string\r\n denominator?: string\r\n /** Medidas que a fórmula combina (a expressão em si fica no servidor). */\r\n refs?: string[]\r\n }\r\n >\r\n}\r\n\r\nexport function modelMeta(model: ModelDef): ModelMeta {\r\n const tableNames = Object.keys(model.tables)\r\n const pick = <T extends FieldMeta & MeasureMeta>(field: T) => ({\r\n ...(field.label !== undefined && { label: field.label }),\r\n ...(field.description !== undefined && { description: field.description }),\r\n ...(field.format !== undefined && { format: field.format }),\r\n ...(field.decimals !== undefined && { decimals: field.decimals })\r\n })\r\n return {\r\n name: model.name,\r\n dimensions: Object.fromEntries(\r\n Object.entries(model.dimensions).map(([name, dim]) => [\r\n name,\r\n { type: dim.type, table: referencedTables(dim.sql, tableNames)[0], ...pick(dim) }\r\n ])\r\n ),\r\n measures: Object.fromEntries(\r\n Object.entries(model.measures).map(([name, measure]) => [\r\n name,\r\n {\r\n type: measure.type,\r\n // Contagem sem formato: inteiro (sem casas decimais)\r\n ...((measure.type === 'count' || measure.type === 'countDistinct') && {\r\n format: 'integer' as const\r\n }),\r\n ...pick(measure),\r\n ...(measure.type === 'ratio' && {\r\n numerator: measure.numerator,\r\n denominator: measure.denominator\r\n }),\r\n ...(measure.type === 'formula' && { refs: formulaRefs(measure.expression) })\r\n }\r\n ])\r\n )\r\n }\r\n}\r\n\r\n/**\r\n * Lista os problemas do modelo (vazio = válido). Confere também a forma do objeto, porque o rascunho\r\n * do devtools chega como JSON da request.\r\n */\r\nexport function validateModel(model: ModelDef): string[] {\r\n if (!isRecord(model)) return ['modelo inválido']\r\n if (!isRecord(model.dimensions) || !isRecord(model.measures))\r\n return ['`dimensions` e `measures` são obrigatórios']\r\n // Rascunho/arquivo antigo: o formato base + joins foi removido\r\n if ('base' in model || 'joins' in model)\r\n return ['o formato `base` + `joins` não existe mais: use `tables` + `relationships`']\r\n if (!isRecord(model.tables)) return ['`tables` é obrigatório']\r\n if (model.relationships !== undefined && !Array.isArray(model.relationships))\r\n return ['`relationships` precisa ser uma lista']\r\n\r\n if (model.diagram !== undefined && !diagramValid(model.diagram))\r\n return ['`diagram` precisa ser { nome: { x, y, width?, height? } } com números (tamanho > 0)']\r\n\r\n const errors: string[] = []\r\n const { tables } = model\r\n const names = [...Object.keys(model.dimensions), ...Object.keys(model.measures)]\r\n\r\n for (const name of [...names, ...Object.keys(tables)]) {\r\n if (!IDENTIFIER.test(name)) errors.push(`nome inválido: \"${name}\"`)\r\n else if (name.startsWith('__')) errors.push(`nome reservado: \"${name}\" (começa com \"__\")`)\r\n }\r\n\r\n const duplicated = names.find((name, index) => names.indexOf(name) !== index)\r\n if (duplicated) errors.push(`nome repetido: \"${duplicated}\"`)\r\n\r\n for (const [name, table] of Object.entries(tables)) {\r\n if (!isRecord(table) || !isText(table.sql)) {\r\n errors.push(`tabela \"${name}\" sem SQL`)\r\n continue\r\n }\r\n if (table.key !== undefined && (typeof table.key !== 'string' || !IDENTIFIER.test(table.key)))\r\n errors.push(`tabela \"${name}\": key inválida`)\r\n errors.push(...metaErrors(`tabela \"${name}\"`, table))\r\n }\r\n\r\n const tableNames = Object.keys(tables)\r\n errors.push(...relationshipErrors(model, tableNames))\r\n\r\n for (const [name, dimension] of Object.entries(model.dimensions)) {\r\n if (!isRecord(dimension) || !isText(dimension.sql)) errors.push(`dimensão \"${name}\" sem SQL`)\r\n else if (dimension.type !== 'number' && dimension.type !== 'string')\r\n errors.push(`dimensão \"${name}\" com tipo inválido`)\r\n else {\r\n errors.push(...metaErrors(`dimensão \"${name}\"`, dimension))\r\n if (referencedTables(dimension.sql, tableNames).length !== 1)\r\n errors.push(`dimensão \"${name}\" precisa usar colunas de uma tabela só (\\`tabela.coluna\\`)`)\r\n }\r\n }\r\n\r\n const edges = modelEdges(model)\r\n for (const [name, measure] of Object.entries(model.measures)) {\r\n if (!isRecord(measure)) {\r\n errors.push(`medida \"${name}\" inválida`)\r\n } else if (isAggregate(measure)) {\r\n if (!isText(measure.sql)) errors.push(`medida \"${name}\" sem SQL`)\r\n else {\r\n const used = referencedTables(measure.sql, tableNames)\r\n if (!used.length)\r\n errors.push(`medida \"${name}\" precisa usar colunas de uma tabela (\\`tabela.coluna\\`)`)\r\n else if (!measureHome(used, edges))\r\n errors.push(\r\n `medida \"${name}\" usa ${used.join(', ')}, mas nenhuma delas chega às outras pelos relacionamentos`\r\n )\r\n }\r\n } else if (measure.type === 'ratio' || measure.type === 'formula') {\r\n if (measure.type === 'formula' && !isText(measure.expression))\r\n errors.push(`medida \"${name}\" sem expressão`)\r\n else if (measure.type === 'formula' && !formulaRefs(measure.expression).length)\r\n errors.push(`medida \"${name}\": a fórmula precisa citar medidas como {nome}`)\r\n for (const ref of measureDeps(measure)) {\r\n if (typeof ref !== 'string' || !model.measures[ref])\r\n errors.push(`medida \"${name}\" referencia \"${ref}\", que não existe`)\r\n }\r\n } else {\r\n errors.push(`medida \"${name}\" com tipo inválido`)\r\n }\r\n if (isRecord(measure)) errors.push(...metaErrors(`medida \"${name}\"`, measure))\r\n }\r\n if (errors.length) return errors\r\n\r\n // Medidas circulares (a -> b -> a) só estourariam na hora de compilar a consulta\r\n const visit = (name: string, stack: string[]): string | null => {\r\n if (stack.includes(name)) return [...stack, name].join(' -> ')\r\n for (const dep of measureDeps(model.measures[name])) {\r\n const cycle = visit(dep, [...stack, name])\r\n if (cycle) return cycle\r\n }\r\n return null\r\n }\r\n for (const name of Object.keys(model.measures)) {\r\n const cycle = visit(name, [])\r\n if (cycle) {\r\n errors.push(`medida circular: ${cycle}`)\r\n break\r\n }\r\n }\r\n\r\n return errors\r\n}\r\n\r\nconst diagramValid = (diagram: unknown) =>\r\n isRecord(diagram) &&\r\n Object.values(diagram).every(\r\n (point) =>\r\n isRecord(point) &&\r\n Number.isFinite(point.x) &&\r\n Number.isFinite(point.y) &&\r\n [point.width, point.height].every(\r\n (size) => size === undefined || (Number.isFinite(size) && (size as number) > 0)\r\n )\r\n )\r\n\r\nfunction relationshipErrors(model: ModelDef, tableNames: string[]): string[] {\r\n const errors: string[] = []\r\n const seen = new Set<string>()\r\n for (const [index, relationship] of (model.relationships ?? []).entries()) {\r\n const context = `relacionamento ${index + 1}`\r\n if (!isRecord(relationship)) {\r\n errors.push(`${context} inválido`)\r\n continue\r\n }\r\n const from = parseColumnRef(relationship.from)\r\n const to = parseColumnRef(relationship.to)\r\n if (!from || !to) {\r\n errors.push(`${context}: use \\`tabela.coluna\\` em \\`from\\` e \\`to\\``)\r\n continue\r\n }\r\n for (const ref of [from, to])\r\n if (!tableNames.includes(ref.table))\r\n errors.push(`${context}: tabela \"${ref.table}\" não existe`)\r\n if (from.table === to.table)\r\n errors.push(`${context}: liga a tabela \"${from.table}\" a ela mesma`)\r\n if (relationship.inner !== undefined && typeof relationship.inner !== 'boolean')\r\n errors.push(`${context}: \\`inner\\` precisa ser true/false`)\r\n const pair = `${from.table}->${to.table}`\r\n if (seen.has(pair))\r\n errors.push(`${context}: já existe um relacionamento de \"${from.table}\" para \"${to.table}\"`)\r\n seen.add(pair)\r\n }\r\n if (errors.length) return errors\r\n\r\n // Dois caminhos até a mesma tabela deixariam o filtro ambíguo (o Power BI deixa um inativo)\r\n const edges = modelEdges(model)\r\n for (const table of tableNames) {\r\n const { ambiguous, paths } = reachFrom(table, edges)\r\n if (ambiguous.length) {\r\n errors.push(\r\n `caminho ambíguo de \"${table}\" até \"${ambiguous[0]}\": há mais de um relacionamento levando lá`\r\n )\r\n break\r\n }\r\n // Ciclo: a tabela alcança a si mesma\r\n if (edges.some((edge) => edge.to.table === table && paths.has(edge.from.table))) {\r\n errors.push(`relacionamentos em ciclo passando por \"${table}\"`)\r\n break\r\n }\r\n }\r\n return errors\r\n}\r\n","import {\r\n type DiagramPosition,\r\n type FieldMeta,\r\n isAggregate,\r\n type MeasureDef,\r\n type MeasureMeta,\r\n type ModelDef,\r\n type TableDef\r\n} from './model'\r\n\r\n/**\r\n * Gera o código TS de um modelo (`defineModel({...})`). Usado pelo \"Copiar TS\" do devtools e pelo\r\n * \"Salvar no arquivo\" do servidor, que troca só a chamada do `defineModel` dentro do arquivo.\r\n */\r\n\r\nconst diagramCode = ({ x, y, width, height }: DiagramPosition) =>\r\n `{ x: ${x}, y: ${y}${width === undefined ? '' : `, width: ${width}`}${height === undefined ? '' : `, height: ${height}`} }`\r\n\r\n/** JSON com as chaves ordenadas: compara modelos sem depender da ordem das propriedades. */\r\nexport function canonicalJson(value: unknown): string {\r\n return JSON.stringify(value, (_, item) =>\r\n item && typeof item === 'object' && !Array.isArray(item)\r\n ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\r\n : item\r\n )\r\n}\r\n\r\nexport function literal(text: string) {\r\n if (text.includes('\\n'))\r\n return `\\`${text.replaceAll('\\\\', '\\\\\\\\').replaceAll('`', '\\\\`').replaceAll('${', '\\\\${')}\\``\r\n return `'${text.replaceAll('\\\\', '\\\\\\\\').replaceAll(\"'\", \"\\\\'\")}'`\r\n}\r\n\r\nfunction metaEntries(field: FieldMeta & MeasureMeta) {\r\n const entries: string[] = []\r\n if (field.label !== undefined) entries.push(`label: ${literal(field.label)}`)\r\n if (field.description !== undefined) entries.push(`description: ${literal(field.description)}`)\r\n if (field.format !== undefined) entries.push(`format: '${field.format}'`)\r\n if (field.decimals !== undefined) entries.push(`decimals: ${field.decimals}`)\r\n return entries\r\n}\r\n\r\nfunction measureCode(measure: MeasureDef) {\r\n const meta = metaEntries(measure)\r\n const args = isAggregate(measure)\r\n ? [literal(measure.sql)]\r\n : measure.type === 'formula'\r\n ? [literal(measure.expression)]\r\n : [literal(measure.numerator), literal(measure.denominator)]\r\n if (meta.length) args.push(`{ ${meta.join(', ')} }`)\r\n return `${measure.type}(${args.join(', ')})`\r\n}\r\n\r\nfunction tableCode(table: TableDef) {\r\n const fields = [\r\n ...(table.key !== undefined ? [`key: ${literal(table.key)}`] : []),\r\n `sql: ${literal(table.sql)}`,\r\n ...metaEntries(table)\r\n ]\r\n return `{ ${fields.join(', ')} }`\r\n}\r\n\r\nconst entries = <T>(record: Record<string, T>, render: (value: T) => string) =>\r\n Object.entries(record)\r\n .map(([name, value]) => ` ${name}: ${render(value)}`)\r\n .join(',\\n')\r\n\r\n/** `{ ... }` com uma entrada por linha, ou `{}` quando vazio (modelo recém-criado). */\r\nconst block = <T>(record: Record<string, T>, render: (value: T) => string) =>\r\n Object.keys(record).length ? `{\\n${entries(record, render)}\\n }` : '{}'\r\n\r\n/** O objeto passado ao `defineModel`, começando em `{` (sem indentação na primeira linha). */\r\nexport function modelObjectCode(model: ModelDef): string {\r\n const lines = ['{', ` name: ${literal(model.name)},`]\r\n lines.push(` tables: ${block(model.tables, tableCode)},`)\r\n if (model.relationships?.length)\r\n lines.push(\r\n ` relationships: [\\n${model.relationships\r\n .map(\r\n (relationship) =>\r\n ` { from: ${literal(relationship.from)}, to: ${literal(relationship.to)}${relationship.inner ? ', inner: true' : ''} }`\r\n )\r\n .join(',\\n')}\\n ],`\r\n )\r\n lines.push(\r\n ` dimensions: ${block(model.dimensions, (dim) =>\r\n [`{ sql: ${literal(dim.sql)}`, `type: '${dim.type}'`, ...metaEntries(dim)]\r\n .join(', ')\r\n .concat(' }')\r\n )},`,\r\n ` measures: ${block(model.measures, measureCode)}${model.diagram ? ',' : ''}`\r\n )\r\n if (model.diagram) lines.push(` diagram: {\\n${entries(model.diagram, diagramCode)}\\n }`)\r\n lines.push('}')\r\n return lines.join('\\n')\r\n}\r\n\r\n/** Helpers do `@bluemarble/bi/server` que o código do modelo usa. */\r\nexport function modelImports(model: ModelDef) {\r\n // O helper tem o nome do tipo (`sum(...)`, `countDistinct(...)`, `formula(...)`)\r\n const types = new Set(Object.values(model.measures).map((measure) => measure.type))\r\n return ['defineModel', ...types].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))\r\n}\r\n\r\n/** Arquivo completo, pronto para colar em `src/dashboard/models/<nome>.ts`. */\r\nexport function toModelCode(model: ModelDef): string {\r\n return [\r\n `import { ${modelImports(model).join(', ')} } from '@bluemarble/bi/server'`,\r\n '',\r\n `export const ${model.name} = defineModel(${modelObjectCode(model)})`,\r\n ''\r\n ].join('\\n')\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"compare-DloZhg1T.js","names":[],"sources":["../src/compare.ts"],"sourcesContent":["import type { DashboardRow } from './types'\n\n/**\n * Compara dois resultados da mesma consulta, casando as linhas pelas dimensões. Usado para conferir\n * rascunho x modelo registrado (devtools) e snapshot x banco (testes de regressão).\n */\n\nexport type ValueChange = { measure: string; expected: unknown; actual: unknown }\n\nexport type RowDiff = {\n /** Valores das dimensões da linha. */\n dimensions: DashboardRow\n /** `missing`: só no esperado; `extra`: só no atual; `changed`: medidas diferentes. */\n kind: 'missing' | 'extra' | 'changed'\n changes: ValueChange[]\n}\n\nexport type CompareOptions = {\n /** Diferença absoluta tolerada entre números (padrão: meio centavo). */\n tolerance?: number\n}\n\nconst sameValue = (expected: unknown, actual: unknown, tolerance: number) => {\n if (expected === null || actual === null || expected === undefined || actual === undefined)\n return (expected ?? null) === (actual ?? null)\n if (typeof expected === 'number' && typeof actual === 'number')\n return Math.abs(expected - actual) <= tolerance\n return String(expected) === String(actual)\n}\n\nexport function diffRows(\n expected: DashboardRow[],\n actual: DashboardRow[],\n query: { dimensions?: string[]; measures?: string[] },\n { tolerance = 0.005 }: CompareOptions = {}\n): RowDiff[] {\n const dimensions = query.dimensions ?? []\n const measures = query.measures ?? []\n const keyOf = (row: DashboardRow) => JSON.stringify(dimensions.map((name) => row[name] ?? null))\n const pick = (row: DashboardRow) =>\n Object.fromEntries(dimensions.map((name) => [name, row[name] ?? null]))\n\n const actualByKey = new Map(actual.map((row) => [keyOf(row), row]))\n const diffs: RowDiff[] = []\n\n for (const row of expected) {\n const key = keyOf(row)\n const other = actualByKey.get(key)\n if (!other) {\n diffs.push({ dimensions: pick(row), kind: 'missing', changes: [] })\n continue\n }\n actualByKey.delete(key)\n const changes = measures\n .filter((measure) => !sameValue(row[measure], other[measure], tolerance))\n .map((measure) => ({ measure, expected: row[measure], actual: other[measure] }))\n if (changes.length) diffs.push({ dimensions: pick(row), kind: 'changed', changes })\n }\n for (const row of actualByKey.values())\n diffs.push({ dimensions: pick(row), kind: 'extra', changes: [] })\n\n return diffs\n}\n\n/** Resumo legível (mensagem de teste, log). */\nexport function describeDiffs(diffs: RowDiff[], limit = 20): string {\n const lines = diffs.slice(0, limit).map((diff) => {\n const where = JSON.stringify(diff.dimensions)\n if (diff.kind === 'missing') return `- ${where}: linha sumiu`\n if (diff.kind === 'extra') return `+ ${where}: linha nova`\n return `~ ${where}: ${diff.changes\n .map((change) => `${change.measure} ${change.expected} -> ${change.actual}`)\n .join(', ')}`\n })\n if (diffs.length > limit) lines.push(`… e mais ${diffs.length - limit}`)\n return lines.join('\\n')\n}\n"],"mappings":";AAsBA,MAAM,aAAa,UAAmB,QAAiB,cAAsB;CAC3E,IAAI,aAAa,QAAQ,WAAW,QAAQ,aAAa,KAAA,KAAa,WAAW,KAAA,GAC/E,QAAQ,YAAY,WAAW,UAAU;CAC3C,IAAI,OAAO,aAAa,YAAY,OAAO,WAAW,UACpD,OAAO,KAAK,IAAI,WAAW,MAAM,KAAK;CACxC,OAAO,OAAO,QAAQ,MAAM,OAAO,MAAM;AAC3C;AAEA,SAAgB,SACd,UACA,QACA,OACA,EAAE,YAAY,SAA0B,CAAC,GAC9B;CACX,MAAM,aAAa,MAAM,cAAc,CAAC;CACxC,MAAM,WAAW,MAAM,YAAY,CAAC;CACpC,MAAM,SAAS,QAAsB,KAAK,UAAU,WAAW,KAAK,SAAS,IAAI,SAAS,IAAI,CAAC;CAC/F,MAAM,QAAQ,QACZ,OAAO,YAAY,WAAW,KAAK,SAAS,CAAC,MAAM,IAAI,SAAS,IAAI,CAAC,CAAC;CAExE,MAAM,cAAc,IAAI,IAAI,OAAO,KAAK,QAAQ,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC;CAClE,MAAM,QAAmB,CAAC;CAE1B,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,MAAM,MAAM,GAAG;EACrB,MAAM,QAAQ,YAAY,IAAI,GAAG;EACjC,IAAI,CAAC,OAAO;GACV,MAAM,KAAK;IAAE,YAAY,KAAK,GAAG;IAAG,MAAM;IAAW,SAAS,CAAC;GAAE,CAAC;GAClE;EACF;EACA,YAAY,OAAO,GAAG;EACtB,MAAM,UAAU,SACb,QAAQ,YAAY,CAAC,UAAU,IAAI,UAAU,MAAM,UAAU,SAAS,CAAC,CAAC,CACxE,KAAK,aAAa;GAAE;GAAS,UAAU,IAAI;GAAU,QAAQ,MAAM;EAAS,EAAE;EACjF,IAAI,QAAQ,QAAQ,MAAM,KAAK;GAAE,YAAY,KAAK,GAAG;GAAG,MAAM;GAAW;EAAQ,CAAC;CACpF;CACA,KAAK,MAAM,OAAO,YAAY,OAAO,GACnC,MAAM,KAAK;EAAE,YAAY,KAAK,GAAG;EAAG,MAAM;EAAS,SAAS,CAAC;CAAE,CAAC;CAElE,OAAO;AACT;;AAGA,SAAgB,cAAc,OAAkB,QAAQ,IAAY;CAClE,MAAM,QAAQ,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,SAAS;EAChD,MAAM,QAAQ,KAAK,UAAU,KAAK,UAAU;EAC5C,IAAI,KAAK,SAAS,WAAW,OAAO,KAAK,MAAM;EAC/C,IAAI,KAAK,SAAS,SAAS,OAAO,KAAK,MAAM;EAC7C,OAAO,KAAK,MAAM,IAAI,KAAK,QACxB,KAAK,WAAW,GAAG,OAAO,QAAQ,GAAG,OAAO,SAAS,MAAM,OAAO,QAAQ,CAAC,CAC3E,KAAK,IAAI;CACd,CAAC;CACD,IAAI,MAAM,SAAS,OAAO,MAAM,KAAK,YAAY,MAAM,SAAS,OAAO;CACvE,OAAO,MAAM,KAAK,IAAI;AACxB"}
1
+ {"version":3,"file":"compare-DloZhg1T.js","names":[],"sources":["../src/compare.ts"],"sourcesContent":["import type { DashboardRow } from './types'\r\n\r\n/**\r\n * Compara dois resultados da mesma consulta, casando as linhas pelas dimensões. Usado para conferir\r\n * rascunho x modelo registrado (devtools) e snapshot x banco (testes de regressão).\r\n */\r\n\r\nexport type ValueChange = { measure: string; expected: unknown; actual: unknown }\r\n\r\nexport type RowDiff = {\r\n /** Valores das dimensões da linha. */\r\n dimensions: DashboardRow\r\n /** `missing`: só no esperado; `extra`: só no atual; `changed`: medidas diferentes. */\r\n kind: 'missing' | 'extra' | 'changed'\r\n changes: ValueChange[]\r\n}\r\n\r\nexport type CompareOptions = {\r\n /** Diferença absoluta tolerada entre números (padrão: meio centavo). */\r\n tolerance?: number\r\n}\r\n\r\nconst sameValue = (expected: unknown, actual: unknown, tolerance: number) => {\r\n if (expected === null || actual === null || expected === undefined || actual === undefined)\r\n return (expected ?? null) === (actual ?? null)\r\n if (typeof expected === 'number' && typeof actual === 'number')\r\n return Math.abs(expected - actual) <= tolerance\r\n return String(expected) === String(actual)\r\n}\r\n\r\nexport function diffRows(\r\n expected: DashboardRow[],\r\n actual: DashboardRow[],\r\n query: { dimensions?: string[]; measures?: string[] },\r\n { tolerance = 0.005 }: CompareOptions = {}\r\n): RowDiff[] {\r\n const dimensions = query.dimensions ?? []\r\n const measures = query.measures ?? []\r\n const keyOf = (row: DashboardRow) => JSON.stringify(dimensions.map((name) => row[name] ?? null))\r\n const pick = (row: DashboardRow) =>\r\n Object.fromEntries(dimensions.map((name) => [name, row[name] ?? null]))\r\n\r\n const actualByKey = new Map(actual.map((row) => [keyOf(row), row]))\r\n const diffs: RowDiff[] = []\r\n\r\n for (const row of expected) {\r\n const key = keyOf(row)\r\n const other = actualByKey.get(key)\r\n if (!other) {\r\n diffs.push({ dimensions: pick(row), kind: 'missing', changes: [] })\r\n continue\r\n }\r\n actualByKey.delete(key)\r\n const changes = measures\r\n .filter((measure) => !sameValue(row[measure], other[measure], tolerance))\r\n .map((measure) => ({ measure, expected: row[measure], actual: other[measure] }))\r\n if (changes.length) diffs.push({ dimensions: pick(row), kind: 'changed', changes })\r\n }\r\n for (const row of actualByKey.values())\r\n diffs.push({ dimensions: pick(row), kind: 'extra', changes: [] })\r\n\r\n return diffs\r\n}\r\n\r\n/** Resumo legível (mensagem de teste, log). */\r\nexport function describeDiffs(diffs: RowDiff[], limit = 20): string {\r\n const lines = diffs.slice(0, limit).map((diff) => {\r\n const where = JSON.stringify(diff.dimensions)\r\n if (diff.kind === 'missing') return `- ${where}: linha sumiu`\r\n if (diff.kind === 'extra') return `+ ${where}: linha nova`\r\n return `~ ${where}: ${diff.changes\r\n .map((change) => `${change.measure} ${change.expected} -> ${change.actual}`)\r\n .join(', ')}`\r\n })\r\n if (diffs.length > limit) lines.push(`… e mais ${diffs.length - limit}`)\r\n return lines.join('\\n')\r\n}\r\n"],"mappings":";AAsBA,MAAM,aAAa,UAAmB,QAAiB,cAAsB;CAC3E,IAAI,aAAa,QAAQ,WAAW,QAAQ,aAAa,KAAA,KAAa,WAAW,KAAA,GAC/E,QAAQ,YAAY,WAAW,UAAU;CAC3C,IAAI,OAAO,aAAa,YAAY,OAAO,WAAW,UACpD,OAAO,KAAK,IAAI,WAAW,MAAM,KAAK;CACxC,OAAO,OAAO,QAAQ,MAAM,OAAO,MAAM;AAC3C;AAEA,SAAgB,SACd,UACA,QACA,OACA,EAAE,YAAY,SAA0B,CAAC,GAC9B;CACX,MAAM,aAAa,MAAM,cAAc,CAAC;CACxC,MAAM,WAAW,MAAM,YAAY,CAAC;CACpC,MAAM,SAAS,QAAsB,KAAK,UAAU,WAAW,KAAK,SAAS,IAAI,SAAS,IAAI,CAAC;CAC/F,MAAM,QAAQ,QACZ,OAAO,YAAY,WAAW,KAAK,SAAS,CAAC,MAAM,IAAI,SAAS,IAAI,CAAC,CAAC;CAExE,MAAM,cAAc,IAAI,IAAI,OAAO,KAAK,QAAQ,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC;CAClE,MAAM,QAAmB,CAAC;CAE1B,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,MAAM,MAAM,GAAG;EACrB,MAAM,QAAQ,YAAY,IAAI,GAAG;EACjC,IAAI,CAAC,OAAO;GACV,MAAM,KAAK;IAAE,YAAY,KAAK,GAAG;IAAG,MAAM;IAAW,SAAS,CAAC;GAAE,CAAC;GAClE;EACF;EACA,YAAY,OAAO,GAAG;EACtB,MAAM,UAAU,SACb,QAAQ,YAAY,CAAC,UAAU,IAAI,UAAU,MAAM,UAAU,SAAS,CAAC,CAAC,CACxE,KAAK,aAAa;GAAE;GAAS,UAAU,IAAI;GAAU,QAAQ,MAAM;EAAS,EAAE;EACjF,IAAI,QAAQ,QAAQ,MAAM,KAAK;GAAE,YAAY,KAAK,GAAG;GAAG,MAAM;GAAW;EAAQ,CAAC;CACpF;CACA,KAAK,MAAM,OAAO,YAAY,OAAO,GACnC,MAAM,KAAK;EAAE,YAAY,KAAK,GAAG;EAAG,MAAM;EAAS,SAAS,CAAC;CAAE,CAAC;CAElE,OAAO;AACT;;AAGA,SAAgB,cAAc,OAAkB,QAAQ,IAAY;CAClE,MAAM,QAAQ,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,SAAS;EAChD,MAAM,QAAQ,KAAK,UAAU,KAAK,UAAU;EAC5C,IAAI,KAAK,SAAS,WAAW,OAAO,KAAK,MAAM;EAC/C,IAAI,KAAK,SAAS,SAAS,OAAO,KAAK,MAAM;EAC7C,OAAO,KAAK,MAAM,IAAI,KAAK,QACxB,KAAK,WAAW,GAAG,OAAO,QAAQ,GAAG,OAAO,SAAS,MAAM,OAAO,QAAQ,CAAC,CAC3E,KAAK,IAAI;CACd,CAAC;CACD,IAAI,MAAM,SAAS,OAAO,MAAM,KAAK,YAAY,MAAM,SAAS,OAAO;CACvE,OAAO,MAAM,KAAK,IAAI;AACxB"}
@@ -133,6 +133,13 @@ type BaseChartDef = {
133
133
  height?: number;
134
134
  /** Aparência do card deste gráfico (sobrepõe o `cards` do layout). */
135
135
  card?: CardStyle;
136
+ /**
137
+ * Dimensões cujos filtros este gráfico NÃO respeita: slicers do painel ou o filtro cruzado de um
138
+ * clique em outro gráfico (como o "Editar interações" do Power BI). Vazio/ausente = respeita todos.
139
+ * Ex.: numa rosca por produto com `clickToFilter`, pôr `["descricaoProduto"]` nas tabelas que devem
140
+ * continuar mostrando o total, sem reagir ao clique na fatia.
141
+ */
142
+ ignoreFilters?: string[];
136
143
  };
137
144
  /**
138
145
  * Formato de uma medida num gráfico: sobrepõe o `format`/`decimals` do dicionário do modelo só
@@ -225,6 +232,8 @@ type DonutChartDef = BaseChartDef & ColumnsDisplay & {
225
232
  type: 'donut';
226
233
  dimension: string;
227
234
  measure: string;
235
+ /** Nome de um parâmetro de medida do layout: a medida passa a seguir o slicer dele. */
236
+ measureParameter?: string;
228
237
  /** Fatias além dessa quantidade viram "Outros". */
229
238
  maxSlices?: number;
230
239
  /** Casas decimais do percentual. Padrão: 1. */
@@ -287,11 +296,17 @@ type TableChartDef = BaseChartDef & ColumnsDisplay & {
287
296
  align?: Record<string, ColumnAlign>;
288
297
  /** Alinhamento vertical por coluna (`top`, `middle`, `bottom`). Padrão: `middle`. */
289
298
  verticalAlign?: Record<string, ColumnVerticalAlign>;
299
+ /** Espaçamento vertical das células (px), em cima e embaixo. Padrão: 8. */
300
+ cellPadding?: number;
301
+ /** Tamanho da fonte das células e do cabeçalho (px). Padrão: 13. */
302
+ fontSize?: number;
290
303
  };
291
304
  type KpiChartDef = Omit<BaseChartDef, 'height'> & ColumnsDisplay & {
292
305
  type: 'kpi';
293
306
  /** Medida do número grande (total com os filtros da página). */
294
307
  measure: string;
308
+ /** Nome de um parâmetro de medida do layout: a medida passa a seguir o slicer dele. */
309
+ measureParameter?: string;
295
310
  /** Medida extra numa linha menor embaixo (ex.: volume, custo/t). */
296
311
  secondary?: string;
297
312
  };
@@ -311,6 +326,29 @@ type FilterDef = {
311
326
  width?: number;
312
327
  /** Nome de um formatter do `<DashboardFilters formatters>` para as opções. */
313
328
  formatLabel?: string;
329
+ /**
330
+ * Ids dos gráficos que NÃO reagem a este filtro (o "Editar interações" do Power BI, pela ótica do
331
+ * slicer). Ausente/vazio = todos reagem. Como os filtros são guardados por dimensão, listar um
332
+ * gráfico aqui equivale a pôr a dimensão deste filtro no `ignoreFilters` dele.
333
+ */
334
+ ignoredBy?: string[];
335
+ };
336
+ /**
337
+ * Parâmetro de medida ("medida dinâmica"): um slicer que escolhe UMA medida entre `measures`. Um
338
+ * gráfico de medida única (rosca, KPI) segue o parâmetro quando aponta para ele em
339
+ * `measureParameter`. É o "parâmetro de campo" do Power BI, só para medidas. Renderizado pelo
340
+ * `<DashboardFilters>`, junto dos filtros.
341
+ */
342
+ type ParameterDef = {
343
+ /** Id do parâmetro no layout (letras, números, "-" e "_"). */
344
+ name: string;
345
+ /** Rótulo do slicer. Padrão: "Medida". */
346
+ label?: string;
347
+ /** Medidas oferecidas (a `default`, ou a primeira, é a inicial). */
348
+ measures: string[];
349
+ /** Medida inicial. Padrão: a primeira de `measures`. */
350
+ default?: string;
351
+ width?: number;
314
352
  };
315
353
  /**
316
354
  * Tela de tamanho fixo: o `<DashboardCanvas>` desenha o dashboard nessas dimensões e o escala para
@@ -339,6 +377,14 @@ type CardStyle = {
339
377
  /** Tamanho do título, em px. */
340
378
  titleSize?: number;
341
379
  titleAlign?: 'left' | 'center' | 'right';
380
+ /** Linha divisória abaixo do título. Padrão: sem divisor. */
381
+ titleDivider?: boolean;
382
+ /** Cor do divisor do título. Padrão: `#e0e0e0`. */
383
+ titleDividerColor?: string;
384
+ /** Espaço entre o título e o divisor, em px (com divisor). Padrão: 4. */
385
+ titleGap?: number;
386
+ /** Espaço entre o título/divisor e o conteúdo do gráfico, em px. Padrão: 8. */
387
+ contentGap?: number;
342
388
  };
343
389
  /** Cards de todos os gráficos do layout: estilo padrão e espaço entre eles. */
344
390
  type CardsStyle = CardStyle & {
@@ -358,6 +404,8 @@ type ChartsLayout = {
358
404
  cards?: CardsStyle;
359
405
  /** Renderizados pelo `<DashboardFilters>`, na ordem. */
360
406
  filters?: FilterDef[];
407
+ /** Parâmetros de medida (slicers que trocam a medida de gráficos), no `<DashboardFilters>`. */
408
+ parameters?: ParameterDef[];
361
409
  charts: ChartDef[];
362
410
  };
363
411
  type FieldSpec = {
@@ -389,6 +437,18 @@ type FieldSpec = {
389
437
  } | {
390
438
  kind: 'dimensions';
391
439
  } |
440
+ /** Lista de dimensões cujos filtros o gráfico ignora (`ignoreFilters`). */
441
+ {
442
+ kind: 'filters';
443
+ } |
444
+ /** Lista de ids de gráficos do layout (ex.: `ignoredBy` de um filtro). */
445
+ {
446
+ kind: 'charts';
447
+ } |
448
+ /** Nome de um parâmetro de medida do layout (ex.: `measureParameter` de uma rosca/KPI). */
449
+ {
450
+ kind: 'parameter';
451
+ } |
392
452
  /**
393
453
  * `percent`: cada medida vira uma coluna de % do total (`%:medida`), que entra no `columnOrder`
394
454
  * como as outras colunas.
@@ -751,4 +811,4 @@ type ColorScale = {
751
811
  };
752
812
  //#endregion
753
813
  export { MeasureMeta as $, ChartNumberFormat as A, FieldSpec as B, BarChartDef as C, CardStyle as D, CanvasDef as E, ComboChartDef as F, resolveChartTypes as G, KpiChartDef as H, CustomChartDef as I, Aggregation as J, validateLayout as K, DonutChartDef as L, ChartTypeSpec as M, ChartTypes as N, CardsStyle as O, ChartsLayout as P, MeasureDef as Q, DonutLegendPosition as R, SqlTestResult as S, BuiltinChartDef as T, TableChartDef as U, FilterDef as V, layoutModelWarnings as W, FieldMeta as X, DimensionDef as Y, FormulaMeasure as Z, ModelColumns as _, ValueRule as a, TableDef as at, SaveModelResult as b, DashboardDevtoolsRequest as c, validateModel as ct, DashboardQueryDebug as d, ModelDef as et, DashboardResult as f, FilterValue as g, DevtoolsCapabilities as h, FormatRule as i, SumMeasure as it, ChartType as j, ChartDef as k, DashboardFilters as l, DashboardSnapshot as m, ColumnAlign as n, RatioMeasure as nt, DashboardBatchQuery as o, ValueFormat as ot, DashboardRow as p, AggregateMeasure as q, ColumnVerticalAlign as r, RelationshipDef as rt, DashboardBatchResult as s, modelMeta as st, ColorScale as t, ModelMeta as tt, DashboardQuery as u, ModelValidation as v, BarValuePosition as w, SchemaTable as x, ProfileStep as y, DonutValueContent as z };
754
- //# sourceMappingURL=formatRules-DXMHY9tU.d.ts.map
814
+ //# sourceMappingURL=formatRules-C1PW2EiL.d.ts.map
@@ -344,6 +344,27 @@ const hideEmpty = {
344
344
  label: "Esconder grupos vazios",
345
345
  section: "Dados"
346
346
  };
347
+ /** Filtros (por dimensão) que o gráfico não respeita; comum a todos os tipos. */
348
+ const ignoreFiltersField = {
349
+ kind: "filters",
350
+ label: "Ignorar filtros",
351
+ hint: "Dimensões cujos filtros este gráfico não respeita (slicers do painel ou clique em outro gráfico); vazio = respeita todos",
352
+ section: "Comportamento"
353
+ };
354
+ /** Vincula a medida do gráfico a um parâmetro de medida do layout (rosca/KPI). */
355
+ const measureParameterField = {
356
+ kind: "parameter",
357
+ label: "Medida pelo parâmetro",
358
+ hint: "Segue o slicer de medida escolhido; vazio = usa a medida fixa acima",
359
+ section: "Dados"
360
+ };
361
+ /** Título da rosca/KPI, com o token `{{medida}}` (vira a descrição da medida atual). */
362
+ const measureTitleField = {
363
+ kind: "text",
364
+ label: "Título",
365
+ hint: "Use {{medida}} para a descrição da medida atual (muda com o parâmetro de medida)",
366
+ section: "Gráfico"
367
+ };
347
368
  /**
348
369
  * Rótulo de cada dimensão/medida do gráfico. Um tipo do app ganha o mesmo editor com um campo
349
370
  * `{ kind: 'labels' }` (o componente recebe `labels`; ver `fieldLabel`) e `{ kind: 'formats' }`.
@@ -371,11 +392,7 @@ const percentDecimals = {
371
392
  };
372
393
  const CHART_FIELDS = {
373
394
  kpi: {
374
- title: {
375
- kind: "text",
376
- label: "Título",
377
- section: "Gráfico"
378
- },
395
+ title: measureTitleField,
379
396
  span: common(0).span,
380
397
  measure: {
381
398
  kind: "measure",
@@ -389,11 +406,13 @@ const CHART_FIELDS = {
389
406
  hint: "Linha menor embaixo do número (ex.: volume, custo/t)",
390
407
  section: "Dados"
391
408
  },
409
+ measureParameter: measureParameterField,
392
410
  labels: {
393
411
  ...LABELS_FIELD,
394
412
  hint: "O da medida principal aparece acima do número; o da secundária, na linha de baixo"
395
413
  },
396
- formats: FORMATS_FIELD
414
+ formats: FORMATS_FIELD,
415
+ ignoreFilters: ignoreFiltersField
397
416
  },
398
417
  combo: {
399
418
  ...common(360),
@@ -445,7 +464,8 @@ const CHART_FIELDS = {
445
464
  section: "Aparência"
446
465
  },
447
466
  labels: LABELS_FIELD,
448
- formats: FORMATS_FIELD
467
+ formats: FORMATS_FIELD,
468
+ ignoreFilters: ignoreFiltersField
449
469
  },
450
470
  bar: {
451
471
  ...common(360),
@@ -601,10 +621,12 @@ const CHART_FIELDS = {
601
621
  section: "Valores"
602
622
  },
603
623
  labels: LABELS_FIELD,
604
- formats: FORMATS_FIELD
624
+ formats: FORMATS_FIELD,
625
+ ignoreFilters: ignoreFiltersField
605
626
  },
606
627
  donut: {
607
628
  ...common(300),
629
+ title: measureTitleField,
608
630
  dimension: {
609
631
  kind: "dimension",
610
632
  required: true,
@@ -618,6 +640,7 @@ const CHART_FIELDS = {
618
640
  label: "Medida",
619
641
  section: "Dados"
620
642
  },
643
+ measureParameter: measureParameterField,
621
644
  maxSlices: {
622
645
  kind: "integer",
623
646
  min: 2,
@@ -669,7 +692,8 @@ const CHART_FIELDS = {
669
692
  },
670
693
  labels: LABELS_FIELD,
671
694
  formats: FORMATS_FIELD,
672
- percentDecimals
695
+ percentDecimals,
696
+ ignoreFilters: ignoreFiltersField
673
697
  },
674
698
  table: {
675
699
  ...common(400),
@@ -744,6 +768,24 @@ const CHART_FIELDS = {
744
768
  label: "Cor das bordas",
745
769
  section: "Aparência"
746
770
  },
771
+ cellPadding: {
772
+ kind: "integer",
773
+ min: 0,
774
+ max: 40,
775
+ default: 8,
776
+ label: "Espaçamento das células (px)",
777
+ hint: "Espaço em cima e embaixo de cada célula",
778
+ section: "Aparência"
779
+ },
780
+ fontSize: {
781
+ kind: "integer",
782
+ min: 8,
783
+ max: 32,
784
+ default: 13,
785
+ label: "Tamanho da fonte (px)",
786
+ hint: "Fonte das células e do cabeçalho",
787
+ section: "Aparência"
788
+ },
747
789
  labels: LABELS_FIELD,
748
790
  formats: FORMATS_FIELD,
749
791
  align: {
@@ -770,7 +812,8 @@ const CHART_FIELDS = {
770
812
  label: "Escalas de cor",
771
813
  hint: "Gradiente numa coluna numérica: a cor vai da mínima à máxima conforme o valor (ex.: 0 a 100). Mínimo/máximo vazios = menor/maior valor da coluna na tabela. As regras ficam por cima.",
772
814
  section: "Formatação condicional"
773
- }
815
+ },
816
+ ignoreFilters: ignoreFiltersField
774
817
  }
775
818
  };
776
819
  const BUILTIN_CHART_TYPES = Object.fromEntries(CHART_TYPES.map((type) => [type, {
@@ -789,11 +832,13 @@ function resolveChartTypes(custom = {}) {
789
832
  const { title, span, height } = common(spec.height ?? 300);
790
833
  const base = spec.height === null ? {
791
834
  title,
792
- span
835
+ span,
836
+ ignoreFilters: ignoreFiltersField
793
837
  } : {
794
838
  title,
795
839
  span,
796
- height
840
+ height,
841
+ ignoreFilters: ignoreFiltersField
797
842
  };
798
843
  types[name] = {
799
844
  label: spec.label,
@@ -852,6 +897,49 @@ const FILTER_FIELDS = {
852
897
  label: "Formatar opções",
853
898
  hint: "Formatters passados ao <DashboardFilters formatters={{ ... }}> da página",
854
899
  section: "Filtro"
900
+ },
901
+ ignoredBy: {
902
+ kind: "charts",
903
+ label: "Gráficos que ignoram",
904
+ hint: "Gráficos que não reagem a este filtro; vazio = todos reagem",
905
+ section: "Filtro"
906
+ }
907
+ };
908
+ /** Campos do formulário de um parâmetro de medida no devtools (ver `ParameterDef`). */
909
+ const PARAMETER_FIELDS = {
910
+ name: {
911
+ kind: "text",
912
+ required: true,
913
+ label: "Id",
914
+ hint: "Identificador do parâmetro (letras, números, \"-\" e \"_\")",
915
+ section: "Filtro"
916
+ },
917
+ label: {
918
+ kind: "text",
919
+ label: "Rótulo",
920
+ hint: "Padrão: \"Medida\"",
921
+ section: "Filtro"
922
+ },
923
+ measures: {
924
+ kind: "measures",
925
+ required: true,
926
+ label: "Medidas (opções)",
927
+ hint: "Medidas que o slicer oferece",
928
+ section: "Filtro"
929
+ },
930
+ default: {
931
+ kind: "column",
932
+ label: "Medida inicial",
933
+ hint: "Padrão: a primeira das opções",
934
+ section: "Filtro"
935
+ },
936
+ width: {
937
+ kind: "integer",
938
+ min: 80,
939
+ max: 800,
940
+ default: 200,
941
+ label: "Largura (px)",
942
+ section: "Filtro"
855
943
  }
856
944
  };
857
945
  const CANVAS_FIELDS = {
@@ -949,6 +1037,37 @@ const CARD_FIELDS = {
949
1037
  default: "left",
950
1038
  label: "Alinhamento do título",
951
1039
  section: "Card"
1040
+ },
1041
+ titleDivider: {
1042
+ kind: "boolean",
1043
+ default: false,
1044
+ label: "Divisor do título",
1045
+ hint: "Linha abaixo do título separando do conteúdo",
1046
+ section: "Card"
1047
+ },
1048
+ titleDividerColor: {
1049
+ kind: "color",
1050
+ default: "#e0e0e0",
1051
+ label: "Cor do divisor",
1052
+ section: "Card"
1053
+ },
1054
+ titleGap: {
1055
+ kind: "integer",
1056
+ min: 0,
1057
+ max: 40,
1058
+ default: 4,
1059
+ label: "Espaço título → divisor (px)",
1060
+ hint: "Só com o divisor ligado",
1061
+ section: "Card"
1062
+ },
1063
+ contentGap: {
1064
+ kind: "integer",
1065
+ min: 0,
1066
+ max: 40,
1067
+ default: 8,
1068
+ label: "Espaço título → conteúdo (px)",
1069
+ hint: "Abaixo do título (ou do divisor), antes do gráfico",
1070
+ section: "Card"
952
1071
  }
953
1072
  };
954
1073
  const gapField = (label) => ({
@@ -1012,10 +1131,13 @@ function fieldError(spec, value) {
1012
1131
  case "dimension":
1013
1132
  case "measure":
1014
1133
  case "formatter":
1015
- case "column": return isName(value) ? null : spec.required ? "é obrigatório" : "precisa ser texto";
1134
+ case "column":
1135
+ case "parameter": return isName(value) ? null : spec.required ? "é obrigatório" : "precisa ser texto";
1016
1136
  case "dimensions":
1017
1137
  case "measures":
1018
1138
  case "columnOrder":
1139
+ case "filters":
1140
+ case "charts":
1019
1141
  if (!Array.isArray(value) || !value.every(isName)) return "precisa ser uma lista de nomes";
1020
1142
  if (spec.required && !value.length) return "precisa de pelo menos um item";
1021
1143
  return new Set(value).size === value.length ? null : "tem item repetido";
@@ -1131,6 +1253,29 @@ function validateLayout(layout, types = BUILTIN_CHART_TYPES) {
1131
1253
  if (dimensions.has(filter.dimension)) errors.push(`${context}: dimensão repetida "${filter.dimension}"`);
1132
1254
  dimensions.add(filter.dimension);
1133
1255
  }
1256
+ if (Array.isArray(filter.ignoredBy)) {
1257
+ for (const id of filter.ignoredBy) if (isName(id) && !ids.has(id)) errors.push(`${context}: gráfico "${id}" de \`ignoredBy\` não existe`);
1258
+ }
1259
+ });
1260
+ }
1261
+ if (layout.parameters !== void 0) {
1262
+ if (!Array.isArray(layout.parameters)) return [...errors, "`parameters` precisa ser uma lista"];
1263
+ const names = /* @__PURE__ */ new Set();
1264
+ layout.parameters.forEach((parameter, index) => {
1265
+ const context = `parâmetro ${index + 1}`;
1266
+ if (!isRecord(parameter)) {
1267
+ errors.push(`${context} inválido`);
1268
+ return;
1269
+ }
1270
+ if (typeof parameter.name !== "string" || !LAYOUT_ID.test(parameter.name)) errors.push(`${context}: \`name\` inválido`);
1271
+ else if (names.has(parameter.name)) errors.push(`${context}: name repetido "${parameter.name}"`);
1272
+ else names.add(parameter.name);
1273
+ if (parameter.label !== void 0 && typeof parameter.label !== "string") errors.push(`${context}: \`label\` precisa ser texto`);
1274
+ if (!Array.isArray(parameter.measures) || !parameter.measures.every(isName)) errors.push(`${context}: \`measures\` precisa ser uma lista de medidas`);
1275
+ else if (!parameter.measures.length) errors.push(`${context}: \`measures\` precisa de pelo menos uma medida`);
1276
+ else if (new Set(parameter.measures).size !== parameter.measures.length) errors.push(`${context}: \`measures\` tem medida repetida`);
1277
+ if (parameter.default !== void 0 && (!isName(parameter.default) || Array.isArray(parameter.measures) && !parameter.measures.includes(parameter.default))) errors.push(`${context}: \`default\` precisa ser uma das medidas`);
1278
+ if (parameter.width !== void 0 && typeof parameter.width !== "number") errors.push(`${context}: \`width\` precisa ser um número`);
1134
1279
  });
1135
1280
  }
1136
1281
  return errors;
@@ -1161,6 +1306,7 @@ function fieldReferences(fields, item) {
1161
1306
  if (value === void 0 || value === "") continue;
1162
1307
  if (spec.kind === "dimension") refs.dimensions.push(value);
1163
1308
  if (spec.kind === "dimensions") refs.dimensions.push(...value);
1309
+ if (spec.kind === "filters") refs.dimensions.push(...value);
1164
1310
  if (spec.kind === "measure") refs.measures.push(value);
1165
1311
  if (spec.kind === "measures") refs.measures.push(...value);
1166
1312
  if (spec.kind === "formatter") refs.formatters.push(value);
@@ -1255,6 +1401,12 @@ function layoutModelWarnings(layout, model, types = BUILTIN_CHART_TYPES) {
1255
1401
  }
1256
1402
  }
1257
1403
  for (const filter of layout.filters ?? []) if (!model.dimensions[filter.dimension]) warnings.push(`filtro "${filter.label || filter.dimension}": dimensão "${filter.dimension}" não existe`);
1404
+ for (const parameter of layout.parameters ?? []) for (const measure of parameter.measures ?? []) if (!model.measures[measure]) warnings.push(`parâmetro "${parameter.label || parameter.name}": medida "${measure}" não existe`);
1405
+ const parameterNames = new Set((layout.parameters ?? []).map((parameter) => parameter.name));
1406
+ for (const chart of layout.charts) {
1407
+ const linked = chart.measureParameter;
1408
+ if (typeof linked === "string" && linked && !parameterNames.has(linked)) warnings.push(`"${chart.title || chart.id}": parâmetro de medida "${linked}" não existe`);
1409
+ }
1258
1410
  return warnings.filter(Boolean);
1259
1411
  }
1260
1412
  const isModelField = (model, name) => Object.hasOwn(model.dimensions, name) || Object.hasOwn(model.measures, name);
@@ -1355,6 +1507,6 @@ function layoutFileContent(layout) {
1355
1507
  return `${JSON.stringify(layout, null, 2)}\n`;
1356
1508
  }
1357
1509
  //#endregion
1358
- export { contrastText as A, DEFAULT_SCALE_MIN_COLOR as C, UNARY_OPERATORS as D, RULE_OPERATOR_LABEL as E, rowRuleStyles as M, VALUE_RULE_OPERATORS as O, COLUMN_VERTICAL_ALIGNS as S, RULE_OPERATORS as T, resolveChartTypes as _, FILTER_FIELDS as a, COLOR_SCALE_TARGETS as b, chartColumns as c, customChartProps as d, filterReferences as f, percentColumn as g, layoutModelWarnings as h, CARD_FIELDS as i, matchesValueRule as j, colorScaleStyles as k, chartOfType as l, layoutFileContent as m, CANVAS_PRESETS as n, cardsGap as o, isBuiltinChart as p, CARDS_FIELDS as r, chartCardStyle as s, BUILTIN_CHART_TYPES as t, chartReferences as u, tableColumns as v, NUMERIC_OPERATORS as w, COLUMN_ALIGNS as x, validateLayout as y };
1510
+ export { colorScaleStyles as A, COLUMN_VERTICAL_ALIGNS as C, RULE_OPERATOR_LABEL as D, RULE_OPERATORS as E, matchesValueRule as M, rowRuleStyles as N, UNARY_OPERATORS as O, COLUMN_ALIGNS as S, NUMERIC_OPERATORS as T, percentColumn as _, FILTER_FIELDS as a, validateLayout as b, chartCardStyle as c, chartReferences as d, customChartProps as f, layoutModelWarnings as g, layoutFileContent as h, CARD_FIELDS as i, contrastText as j, VALUE_RULE_OPERATORS as k, chartColumns as l, isBuiltinChart as m, CANVAS_PRESETS as n, PARAMETER_FIELDS as o, filterReferences as p, CARDS_FIELDS as r, cardsGap as s, BUILTIN_CHART_TYPES as t, chartOfType as u, resolveChartTypes as v, DEFAULT_SCALE_MIN_COLOR as w, COLOR_SCALE_TARGETS as x, tableColumns as y };
1359
1511
 
1360
- //# sourceMappingURL=layout-BS3nO6OT.js.map
1512
+ //# sourceMappingURL=layout-BuZhfoRI.js.map