@bluemarble/bi 0.4.0 → 0.6.0
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/dist/{DashboardDevtools-7L93aV1-.js → DashboardDevtools-SCewfQGR.js} +1081 -240
- package/dist/DashboardDevtools-SCewfQGR.js.map +1 -0
- package/dist/{ModelDictionary-Y7W9oK1J.js → ModelDictionary-C2tzlOY8.js} +82 -5
- package/dist/ModelDictionary-C2tzlOY8.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +164 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/index.d.ts +32 -3
- package/dist/client/index.js +79 -100
- package/dist/client/index.js.map +1 -1
- package/dist/codegen-DBe4-XW_.js +354 -0
- package/dist/codegen-DBe4-XW_.js.map +1 -0
- package/dist/{types-HXAn-3I1.d.ts → layout-B7eYgNyR.d.ts} +236 -150
- package/dist/{layout-676sEvvb.js → layout-BS51SQcJ.js} +242 -345
- package/dist/layout-BS51SQcJ.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +4 -1
- package/dist/DashboardDevtools-7L93aV1-.js.map +0 -1
- package/dist/ModelDictionary-Y7W9oK1J.js.map +0 -1
- package/dist/layout-676sEvvb.js.map +0 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
//#region src/star.ts
|
|
2
|
+
/** `'tabela.coluna'` -> `{ table, column }` (nulo se não tiver esse formato). */
|
|
3
|
+
function parseColumnRef(ref) {
|
|
4
|
+
if (typeof ref !== "string") return null;
|
|
5
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(ref.trim());
|
|
6
|
+
return match ? {
|
|
7
|
+
table: match[1],
|
|
8
|
+
column: match[2]
|
|
9
|
+
} : null;
|
|
10
|
+
}
|
|
11
|
+
/** Relacionamentos válidos do modelo, como arestas N -> 1. */
|
|
12
|
+
function modelEdges(model) {
|
|
13
|
+
return (model.relationships ?? []).flatMap((relationship, index) => {
|
|
14
|
+
const from = parseColumnRef(relationship?.from);
|
|
15
|
+
const to = parseColumnRef(relationship?.to);
|
|
16
|
+
return from && to ? [{
|
|
17
|
+
from,
|
|
18
|
+
to,
|
|
19
|
+
index,
|
|
20
|
+
inner: relationship.inner === true
|
|
21
|
+
}] : [];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
/**
|
|
26
|
+
* Tabelas citadas no SQL como `tabela.coluna`. O que está entre aspas simples é ignorado, para
|
|
27
|
+
* `'a.b'` num literal não contar como referência.
|
|
28
|
+
*/
|
|
29
|
+
function referencedTables(sql, tables) {
|
|
30
|
+
const code = sql.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
31
|
+
return tables.filter((name) => new RegExp(`(?<![\\w.\`])${escapeRegex(name)}\\.`).test(code));
|
|
32
|
+
}
|
|
33
|
+
/** Busca em largura a partir de `start`, seguindo os relacionamentos no sentido N -> 1. */
|
|
34
|
+
function reachFrom(start, edges) {
|
|
35
|
+
const paths = /* @__PURE__ */ new Map([[start, []]]);
|
|
36
|
+
let frontier = [start];
|
|
37
|
+
while (frontier.length) {
|
|
38
|
+
const next = [];
|
|
39
|
+
for (const table of frontier) for (const edge of edges) {
|
|
40
|
+
if (edge.from.table !== table || paths.has(edge.to.table)) continue;
|
|
41
|
+
paths.set(edge.to.table, [...paths.get(table) ?? [], {
|
|
42
|
+
table: edge.to.table,
|
|
43
|
+
edge
|
|
44
|
+
}]);
|
|
45
|
+
next.push(edge.to.table);
|
|
46
|
+
}
|
|
47
|
+
frontier = next;
|
|
48
|
+
}
|
|
49
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const edge of edges) if (paths.has(edge.from.table)) incoming.set(edge.to.table, (incoming.get(edge.to.table) ?? 0) + 1);
|
|
51
|
+
return {
|
|
52
|
+
paths,
|
|
53
|
+
ambiguous: [...incoming].filter(([table, count]) => table !== start && count > 1).map(([table]) => table)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Tabelas que precisam entrar na consulta, na ordem do JOIN, para que a partir de `home` se chegue
|
|
58
|
+
* a todas as `targets`. Como cada tabela é alcançada por um caminho só (o modelo valida ambiguidade),
|
|
59
|
+
* a união dos caminhos é uma árvore: cada tabela entra uma vez, ligada pela aresta de quem veio antes.
|
|
60
|
+
*/
|
|
61
|
+
function joinPlan(home, targets, edges) {
|
|
62
|
+
const { paths } = reachFrom(home, edges);
|
|
63
|
+
const plan = [];
|
|
64
|
+
const added = /* @__PURE__ */ new Set([home]);
|
|
65
|
+
const include = (target) => {
|
|
66
|
+
for (const step of paths.get(target) ?? []) {
|
|
67
|
+
if (added.has(step.table)) continue;
|
|
68
|
+
added.add(step.table);
|
|
69
|
+
plan.push(step);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
for (const target of targets) include(target);
|
|
73
|
+
for (let grew = true; grew;) {
|
|
74
|
+
grew = false;
|
|
75
|
+
for (const edge of edges) if (edge.inner && added.has(edge.from.table) && !added.has(edge.to.table)) {
|
|
76
|
+
include(edge.to.table);
|
|
77
|
+
grew = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const depth = (table) => paths.get(table)?.length ?? 0;
|
|
81
|
+
return plan.sort((a, b) => depth(a.table) - depth(b.table));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Tabela de origem de uma medida `sum`: entre as tabelas que o SQL cita, a que alcança todas as
|
|
85
|
+
* outras (ex.: `fPesagem.peso * dProduto.fator` agrega em `fPesagem`). Nulo se nenhuma alcança.
|
|
86
|
+
*/
|
|
87
|
+
function measureHome(tables, edges) {
|
|
88
|
+
if (!tables.length) return null;
|
|
89
|
+
return tables.find((candidate) => {
|
|
90
|
+
const { paths } = reachFrom(candidate, edges);
|
|
91
|
+
return tables.every((table) => paths.has(table));
|
|
92
|
+
}) ?? null;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/model.ts
|
|
96
|
+
const VALUE_FORMATS = [
|
|
97
|
+
"number",
|
|
98
|
+
"integer",
|
|
99
|
+
"currency",
|
|
100
|
+
"percent"
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* Agregações sobre uma expressão SQL (`tabela.coluna`). Todas valem nos dois formatos; no estrela,
|
|
104
|
+
* agregam no grão da tabela de origem, como a soma.
|
|
105
|
+
*/
|
|
106
|
+
const AGGREGATIONS = [
|
|
107
|
+
"sum",
|
|
108
|
+
"count",
|
|
109
|
+
"countDistinct",
|
|
110
|
+
"avg",
|
|
111
|
+
"min",
|
|
112
|
+
"max"
|
|
113
|
+
];
|
|
114
|
+
const AGGREGATION_LABEL = {
|
|
115
|
+
sum: "Soma",
|
|
116
|
+
count: "Contagem",
|
|
117
|
+
countDistinct: "Contagem distinta",
|
|
118
|
+
avg: "Média",
|
|
119
|
+
min: "Mínimo",
|
|
120
|
+
max: "Máximo"
|
|
121
|
+
};
|
|
122
|
+
const isAggregate = (measure) => AGGREGATIONS.includes(measure.type);
|
|
123
|
+
/** O SQL agregado da medida: `SUM(x)`, `COUNT(DISTINCT x)`, `AVG(x)`... */
|
|
124
|
+
function aggregateSql(measure) {
|
|
125
|
+
if (measure.type === "countDistinct") return `COUNT(DISTINCT ${measure.sql})`;
|
|
126
|
+
return `${{
|
|
127
|
+
sum: "SUM",
|
|
128
|
+
count: "COUNT",
|
|
129
|
+
avg: "AVG",
|
|
130
|
+
min: "MIN",
|
|
131
|
+
max: "MAX"
|
|
132
|
+
}[measure.type]}(${measure.sql})`;
|
|
133
|
+
}
|
|
134
|
+
/** Nomes citados como `{nome}` numa fórmula. */
|
|
135
|
+
const formulaRefs = (expression) => [...new Set([...expression.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((match) => match[1]))];
|
|
136
|
+
/** Medidas que uma medida usa diretamente (razão e fórmula). */
|
|
137
|
+
function measureDeps(measure) {
|
|
138
|
+
if (measure.type === "ratio") return [measure.numerator, measure.denominator];
|
|
139
|
+
if (measure.type === "formula") return formulaRefs(measure.expression);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
143
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
144
|
+
const isText = (value) => typeof value === "string" && !!value.trim();
|
|
145
|
+
function metaErrors(context, field) {
|
|
146
|
+
const errors = [];
|
|
147
|
+
for (const key of ["label", "description"]) if (field[key] !== void 0 && typeof field[key] !== "string") errors.push(`${context}: \`${key}\` precisa ser texto`);
|
|
148
|
+
if (field.format !== void 0 && !VALUE_FORMATS.includes(field.format)) errors.push(`${context}: formato inválido "${field.format}"`);
|
|
149
|
+
const { decimals } = field;
|
|
150
|
+
if (decimals !== void 0 && (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 10)) errors.push(`${context}: \`decimals\` precisa ser um inteiro entre 0 e 10`);
|
|
151
|
+
return errors;
|
|
152
|
+
}
|
|
153
|
+
function modelMeta(model) {
|
|
154
|
+
const pick = (field) => ({
|
|
155
|
+
...field.label !== void 0 && { label: field.label },
|
|
156
|
+
...field.description !== void 0 && { description: field.description },
|
|
157
|
+
...field.format !== void 0 && { format: field.format },
|
|
158
|
+
...field.decimals !== void 0 && { decimals: field.decimals }
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
name: model.name,
|
|
162
|
+
dimensions: Object.fromEntries(Object.entries(model.dimensions).map(([name, dim]) => [name, {
|
|
163
|
+
type: dim.type,
|
|
164
|
+
...pick(dim)
|
|
165
|
+
}])),
|
|
166
|
+
measures: Object.fromEntries(Object.entries(model.measures).map(([name, measure]) => [name, {
|
|
167
|
+
type: measure.type,
|
|
168
|
+
...(measure.type === "count" || measure.type === "countDistinct") && { format: "integer" },
|
|
169
|
+
...pick(measure),
|
|
170
|
+
...measure.type === "ratio" && {
|
|
171
|
+
numerator: measure.numerator,
|
|
172
|
+
denominator: measure.denominator
|
|
173
|
+
},
|
|
174
|
+
...measure.type === "formula" && { refs: formulaRefs(measure.expression) }
|
|
175
|
+
}]))
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Lista os problemas do modelo (vazio = válido). Confere também a forma do objeto, porque o rascunho
|
|
180
|
+
* do devtools chega como JSON da request.
|
|
181
|
+
*/
|
|
182
|
+
function validateModel(model) {
|
|
183
|
+
if (!isRecord(model)) return ["modelo inválido"];
|
|
184
|
+
if (!isRecord(model.dimensions) || !isRecord(model.measures)) return ["`dimensions` e `measures` são obrigatórios"];
|
|
185
|
+
if ("base" in model || "joins" in model) return ["o formato `base` + `joins` não existe mais: use `tables` + `relationships`"];
|
|
186
|
+
if (!isRecord(model.tables)) return ["`tables` é obrigatório"];
|
|
187
|
+
if (model.relationships !== void 0 && !Array.isArray(model.relationships)) return ["`relationships` precisa ser uma lista"];
|
|
188
|
+
if (model.diagram !== void 0 && !diagramValid(model.diagram)) return ["`diagram` precisa ser { nome: { x, y, width?, height? } } com números (tamanho > 0)"];
|
|
189
|
+
const errors = [];
|
|
190
|
+
const { tables } = model;
|
|
191
|
+
const names = [...Object.keys(model.dimensions), ...Object.keys(model.measures)];
|
|
192
|
+
for (const name of [...names, ...Object.keys(tables)]) if (!IDENTIFIER.test(name)) errors.push(`nome inválido: "${name}"`);
|
|
193
|
+
else if (name.startsWith("__")) errors.push(`nome reservado: "${name}" (começa com "__")`);
|
|
194
|
+
const duplicated = names.find((name, index) => names.indexOf(name) !== index);
|
|
195
|
+
if (duplicated) errors.push(`nome repetido: "${duplicated}"`);
|
|
196
|
+
for (const [name, table] of Object.entries(tables)) {
|
|
197
|
+
if (!isRecord(table) || !isText(table.sql)) {
|
|
198
|
+
errors.push(`tabela "${name}" sem SQL`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (table.key !== void 0 && (typeof table.key !== "string" || !IDENTIFIER.test(table.key))) errors.push(`tabela "${name}": key inválida`);
|
|
202
|
+
errors.push(...metaErrors(`tabela "${name}"`, table));
|
|
203
|
+
}
|
|
204
|
+
const tableNames = Object.keys(tables);
|
|
205
|
+
errors.push(...relationshipErrors(model, tableNames));
|
|
206
|
+
for (const [name, dimension] of Object.entries(model.dimensions)) if (!isRecord(dimension) || !isText(dimension.sql)) errors.push(`dimensão "${name}" sem SQL`);
|
|
207
|
+
else if (dimension.type !== "number" && dimension.type !== "string") errors.push(`dimensão "${name}" com tipo inválido`);
|
|
208
|
+
else {
|
|
209
|
+
errors.push(...metaErrors(`dimensão "${name}"`, dimension));
|
|
210
|
+
if (referencedTables(dimension.sql, tableNames).length !== 1) errors.push(`dimensão "${name}" precisa usar colunas de uma tabela só (\`tabela.coluna\`)`);
|
|
211
|
+
}
|
|
212
|
+
const edges = modelEdges(model);
|
|
213
|
+
for (const [name, measure] of Object.entries(model.measures)) {
|
|
214
|
+
if (!isRecord(measure)) errors.push(`medida "${name}" inválida`);
|
|
215
|
+
else if (isAggregate(measure)) {
|
|
216
|
+
if (!isText(measure.sql)) errors.push(`medida "${name}" sem SQL`);
|
|
217
|
+
else {
|
|
218
|
+
const used = referencedTables(measure.sql, tableNames);
|
|
219
|
+
if (!used.length) errors.push(`medida "${name}" precisa usar colunas de uma tabela (\`tabela.coluna\`)`);
|
|
220
|
+
else if (!measureHome(used, edges)) errors.push(`medida "${name}" usa ${used.join(", ")}, mas nenhuma delas chega às outras pelos relacionamentos`);
|
|
221
|
+
}
|
|
222
|
+
} else if (measure.type === "ratio" || measure.type === "formula") {
|
|
223
|
+
if (measure.type === "formula" && !isText(measure.expression)) errors.push(`medida "${name}" sem expressão`);
|
|
224
|
+
else if (measure.type === "formula" && !formulaRefs(measure.expression).length) errors.push(`medida "${name}": a fórmula precisa citar medidas como {nome}`);
|
|
225
|
+
for (const ref of measureDeps(measure)) if (typeof ref !== "string" || !model.measures[ref]) errors.push(`medida "${name}" referencia "${ref}", que não existe`);
|
|
226
|
+
} else errors.push(`medida "${name}" com tipo inválido`);
|
|
227
|
+
if (isRecord(measure)) errors.push(...metaErrors(`medida "${name}"`, measure));
|
|
228
|
+
}
|
|
229
|
+
if (errors.length) return errors;
|
|
230
|
+
const visit = (name, stack) => {
|
|
231
|
+
if (stack.includes(name)) return [...stack, name].join(" -> ");
|
|
232
|
+
for (const dep of measureDeps(model.measures[name])) {
|
|
233
|
+
const cycle = visit(dep, [...stack, name]);
|
|
234
|
+
if (cycle) return cycle;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
for (const name of Object.keys(model.measures)) {
|
|
239
|
+
const cycle = visit(name, []);
|
|
240
|
+
if (cycle) {
|
|
241
|
+
errors.push(`medida circular: ${cycle}`);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return errors;
|
|
246
|
+
}
|
|
247
|
+
const diagramValid = (diagram) => isRecord(diagram) && Object.values(diagram).every((point) => isRecord(point) && Number.isFinite(point.x) && Number.isFinite(point.y) && [point.width, point.height].every((size) => size === void 0 || Number.isFinite(size) && size > 0));
|
|
248
|
+
function relationshipErrors(model, tableNames) {
|
|
249
|
+
const errors = [];
|
|
250
|
+
const seen = /* @__PURE__ */ new Set();
|
|
251
|
+
for (const [index, relationship] of (model.relationships ?? []).entries()) {
|
|
252
|
+
const context = `relacionamento ${index + 1}`;
|
|
253
|
+
if (!isRecord(relationship)) {
|
|
254
|
+
errors.push(`${context} inválido`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const from = parseColumnRef(relationship.from);
|
|
258
|
+
const to = parseColumnRef(relationship.to);
|
|
259
|
+
if (!from || !to) {
|
|
260
|
+
errors.push(`${context}: use \`tabela.coluna\` em \`from\` e \`to\``);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const ref of [from, to]) if (!tableNames.includes(ref.table)) errors.push(`${context}: tabela "${ref.table}" não existe`);
|
|
264
|
+
if (from.table === to.table) errors.push(`${context}: liga a tabela "${from.table}" a ela mesma`);
|
|
265
|
+
if (relationship.inner !== void 0 && typeof relationship.inner !== "boolean") errors.push(`${context}: \`inner\` precisa ser true/false`);
|
|
266
|
+
const pair = `${from.table}->${to.table}`;
|
|
267
|
+
if (seen.has(pair)) errors.push(`${context}: já existe um relacionamento de "${from.table}" para "${to.table}"`);
|
|
268
|
+
seen.add(pair);
|
|
269
|
+
}
|
|
270
|
+
if (errors.length) return errors;
|
|
271
|
+
const edges = modelEdges(model);
|
|
272
|
+
for (const table of tableNames) {
|
|
273
|
+
const { ambiguous, paths } = reachFrom(table, edges);
|
|
274
|
+
if (ambiguous.length) {
|
|
275
|
+
errors.push(`caminho ambíguo de "${table}" até "${ambiguous[0]}": há mais de um relacionamento levando lá`);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (edges.some((edge) => edge.to.table === table && paths.has(edge.from.table))) {
|
|
279
|
+
errors.push(`relacionamentos em ciclo passando por "${table}"`);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return errors;
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/codegen.ts
|
|
287
|
+
/**
|
|
288
|
+
* Gera o código TS de um modelo (`defineModel({...})`). Usado pelo "Copiar TS" do devtools e pelo
|
|
289
|
+
* "Salvar no arquivo" do servidor, que troca só a chamada do `defineModel` dentro do arquivo.
|
|
290
|
+
*/
|
|
291
|
+
const diagramCode = ({ x, y, width, height }) => `{ x: ${x}, y: ${y}${width === void 0 ? "" : `, width: ${width}`}${height === void 0 ? "" : `, height: ${height}`} }`;
|
|
292
|
+
/** JSON com as chaves ordenadas: compara modelos sem depender da ordem das propriedades. */
|
|
293
|
+
function canonicalJson(value) {
|
|
294
|
+
return JSON.stringify(value, (_, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item);
|
|
295
|
+
}
|
|
296
|
+
function literal(text) {
|
|
297
|
+
if (text.includes("\n")) return `\`${text.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")}\``;
|
|
298
|
+
return `'${text.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
299
|
+
}
|
|
300
|
+
function metaEntries(field) {
|
|
301
|
+
const entries = [];
|
|
302
|
+
if (field.label !== void 0) entries.push(`label: ${literal(field.label)}`);
|
|
303
|
+
if (field.description !== void 0) entries.push(`description: ${literal(field.description)}`);
|
|
304
|
+
if (field.format !== void 0) entries.push(`format: '${field.format}'`);
|
|
305
|
+
if (field.decimals !== void 0) entries.push(`decimals: ${field.decimals}`);
|
|
306
|
+
return entries;
|
|
307
|
+
}
|
|
308
|
+
function measureCode(measure) {
|
|
309
|
+
const meta = metaEntries(measure);
|
|
310
|
+
const args = isAggregate(measure) ? [literal(measure.sql)] : measure.type === "formula" ? [literal(measure.expression)] : [literal(measure.numerator), literal(measure.denominator)];
|
|
311
|
+
if (meta.length) args.push(`{ ${meta.join(", ")} }`);
|
|
312
|
+
return `${measure.type}(${args.join(", ")})`;
|
|
313
|
+
}
|
|
314
|
+
function tableCode(table) {
|
|
315
|
+
return `{ ${[
|
|
316
|
+
...table.key !== void 0 ? [`key: ${literal(table.key)}`] : [],
|
|
317
|
+
`sql: ${literal(table.sql)}`,
|
|
318
|
+
...metaEntries(table)
|
|
319
|
+
].join(", ")} }`;
|
|
320
|
+
}
|
|
321
|
+
const entries = (record, render) => Object.entries(record).map(([name, value]) => ` ${name}: ${render(value)}`).join(",\n");
|
|
322
|
+
/** `{ ... }` com uma entrada por linha, ou `{}` quando vazio (modelo recém-criado). */
|
|
323
|
+
const block = (record, render) => Object.keys(record).length ? `{\n${entries(record, render)}\n }` : "{}";
|
|
324
|
+
/** O objeto passado ao `defineModel`, começando em `{` (sem indentação na primeira linha). */
|
|
325
|
+
function modelObjectCode(model) {
|
|
326
|
+
const lines = ["{", ` name: ${literal(model.name)},`];
|
|
327
|
+
lines.push(` tables: ${block(model.tables, tableCode)},`);
|
|
328
|
+
if (model.relationships?.length) lines.push(` relationships: [\n${model.relationships.map((relationship) => ` { from: ${literal(relationship.from)}, to: ${literal(relationship.to)}${relationship.inner ? ", inner: true" : ""} }`).join(",\n")}\n ],`);
|
|
329
|
+
lines.push(` dimensions: ${block(model.dimensions, (dim) => [
|
|
330
|
+
`{ sql: ${literal(dim.sql)}`,
|
|
331
|
+
`type: '${dim.type}'`,
|
|
332
|
+
...metaEntries(dim)
|
|
333
|
+
].join(", ").concat(" }"))},`, ` measures: ${block(model.measures, measureCode)}${model.diagram ? "," : ""}`);
|
|
334
|
+
if (model.diagram) lines.push(` diagram: {\n${entries(model.diagram, diagramCode)}\n }`);
|
|
335
|
+
lines.push("}");
|
|
336
|
+
return lines.join("\n");
|
|
337
|
+
}
|
|
338
|
+
/** Helpers do `@bluemarble/bi/server` que o código do modelo usa. */
|
|
339
|
+
function modelImports(model) {
|
|
340
|
+
return ["defineModel", ...new Set(Object.values(model.measures).map((measure) => measure.type))].sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
341
|
+
}
|
|
342
|
+
/** Arquivo completo, pronto para colar em `src/dashboard/models/<nome>.ts`. */
|
|
343
|
+
function toModelCode(model) {
|
|
344
|
+
return [
|
|
345
|
+
`import { ${modelImports(model).join(", ")} } from '@bluemarble/bi/server'`,
|
|
346
|
+
"",
|
|
347
|
+
`export const ${model.name} = defineModel(${modelObjectCode(model)})`,
|
|
348
|
+
""
|
|
349
|
+
].join("\n");
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
export { referencedTables as _, AGGREGATIONS as a, aggregateSql as c, validateModel as d, joinPlan as f, reachFrom as g, parseColumnRef as h, toModelCode as i, isAggregate as l, modelEdges as m, modelImports as n, AGGREGATION_LABEL as o, measureHome as p, modelObjectCode as r, VALUE_FORMATS as s, canonicalJson as t, modelMeta as u };
|
|
353
|
+
|
|
354
|
+
//# sourceMappingURL=codegen-DBe4-XW_.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codegen-DBe4-XW_.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 dimensions: Record<string, FieldMeta & { type: DimensionDef['type'] }>\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 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, ...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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"}
|