@bluemarble/bi 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -77
- package/dist/{DashboardDevtools-D0pzwSgn.js → DashboardDevtools-TFwjFdqn.js} +1437 -565
- package/dist/DashboardDevtools-TFwjFdqn.js.map +1 -0
- package/dist/{ModelDictionary-Cw4sa8tW.js → ModelDictionary-g5d84D0n.js} +67 -5
- package/dist/ModelDictionary-g5d84D0n.js.map +1 -0
- package/dist/cli.js +0 -0
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +48 -9
- package/dist/client/index.js +186 -45
- package/dist/client/index.js.map +1 -1
- package/dist/codegen-LoQzFAle.js.map +1 -1
- package/dist/compare-DloZhg1T.js.map +1 -1
- package/dist/{formatRules-DxA9vMvz.d.ts → formatRules-C1PW2EiL.d.ts} +74 -2
- package/dist/{layout-BS3nO6OT.js → layout-BuZhfoRI.js} +167 -15
- package/dist/layout-BuZhfoRI.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +17 -1
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +81 -81
- package/dist/DashboardDevtools-D0pzwSgn.js.map +0 -1
- package/dist/ModelDictionary-Cw4sa8tW.js.map +0 -1
- package/dist/layout-BS3nO6OT.js.map +0 -1
|
@@ -1 +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.
|
|
@@ -535,6 +595,11 @@ type DashboardDevtoolsRequest = {
|
|
|
535
595
|
columns?: boolean;
|
|
536
596
|
/** Executa cada pedaço do modelo (`devtools.model` ou o registrado) contra o banco. */
|
|
537
597
|
validate?: boolean;
|
|
598
|
+
/**
|
|
599
|
+
* Executa um SELECT avulso (o SQL de uma tabela do modelo, por exemplo) com poucas linhas e
|
|
600
|
+
* devolve as colunas e a amostra em `result.devtools.sqlTest`.
|
|
601
|
+
*/
|
|
602
|
+
testSql?: string;
|
|
538
603
|
/** Grava o rascunho no arquivo do modelo (`preview` só devolve o conteúdo novo). */
|
|
539
604
|
save?: 'preview' | 'write';
|
|
540
605
|
/** Grava o layout de gráficos em `<layoutsDir>/<id>.json` (`preview` só devolve o conteúdo novo). */
|
|
@@ -621,6 +686,12 @@ type ModelValidation = {
|
|
|
621
686
|
warnings: Record<string, string>;
|
|
622
687
|
columns: ModelColumns;
|
|
623
688
|
};
|
|
689
|
+
/** Amostra de um SQL avulso (`devtools.testSql`). */
|
|
690
|
+
type SqlTestResult = {
|
|
691
|
+
columns: string[];
|
|
692
|
+
rows: DashboardRow[];
|
|
693
|
+
durationMs: number;
|
|
694
|
+
};
|
|
624
695
|
type SaveModelResult = {
|
|
625
696
|
path: string;
|
|
626
697
|
previous: string;
|
|
@@ -662,6 +733,7 @@ type DashboardResult = {
|
|
|
662
733
|
profile?: ProfileStep[];
|
|
663
734
|
schema?: SchemaTable[];
|
|
664
735
|
validation?: ModelValidation;
|
|
736
|
+
sqlTest?: SqlTestResult;
|
|
665
737
|
columns?: ModelColumns;
|
|
666
738
|
/** Pedaços que o banco recusou ao ler as colunas (mesmas chaves da validação). */
|
|
667
739
|
columnErrors?: Record<string, string>;
|
|
@@ -738,5 +810,5 @@ type ColorScale = {
|
|
|
738
810
|
target?: ColorScaleTarget;
|
|
739
811
|
};
|
|
740
812
|
//#endregion
|
|
741
|
-
export {
|
|
742
|
-
//# sourceMappingURL=formatRules-
|
|
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 };
|
|
814
|
+
//# sourceMappingURL=formatRules-C1PW2EiL.d.ts.map
|