@pbiplint/core 0.1.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/LICENSE +661 -0
- package/NOTICE +29 -0
- package/README.md +34 -0
- package/dist/engine/config.d.ts +33 -0
- package/dist/engine/config.d.ts.map +1 -0
- package/dist/engine/config.js +61 -0
- package/dist/engine/config.js.map +1 -0
- package/dist/engine/ignore.d.ts +8 -0
- package/dist/engine/ignore.d.ts.map +1 -0
- package/dist/engine/ignore.js +17 -0
- package/dist/engine/ignore.js.map +1 -0
- package/dist/engine/lint.d.ts +38 -0
- package/dist/engine/lint.d.ts.map +1 -0
- package/dist/engine/lint.js +33 -0
- package/dist/engine/lint.js.map +1 -0
- package/dist/engine/rank.d.ts +24 -0
- package/dist/engine/rank.d.ts.map +1 -0
- package/dist/engine/rank.js +38 -0
- package/dist/engine/rank.js.map +1 -0
- package/dist/engine/run.d.ts +21 -0
- package/dist/engine/run.d.ts.map +1 -0
- package/dist/engine/run.js +43 -0
- package/dist/engine/run.js.map +1 -0
- package/dist/format/index.d.ts +11 -0
- package/dist/format/index.d.ts.map +1 -0
- package/dist/format/index.js +21 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/json.d.ts +4 -0
- package/dist/format/json.d.ts.map +1 -0
- package/dist/format/json.js +20 -0
- package/dist/format/json.js.map +1 -0
- package/dist/format/markdown.d.ts +4 -0
- package/dist/format/markdown.d.ts.map +1 -0
- package/dist/format/markdown.js +28 -0
- package/dist/format/markdown.js.map +1 -0
- package/dist/format/sarif.d.ts +4 -0
- package/dist/format/sarif.d.ts.map +1 -0
- package/dist/format/sarif.js +77 -0
- package/dist/format/sarif.js.map +1 -0
- package/dist/format/text.d.ts +31 -0
- package/dist/format/text.d.ts.map +1 -0
- package/dist/format/text.js +57 -0
- package/dist/format/text.js.map +1 -0
- package/dist/index/build.d.ts +11 -0
- package/dist/index/build.d.ts.map +1 -0
- package/dist/index/build.js +11 -0
- package/dist/index/build.js.map +1 -0
- package/dist/index/references.d.ts +37 -0
- package/dist/index/references.d.ts.map +1 -0
- package/dist/index/references.js +126 -0
- package/dist/index/references.js.map +1 -0
- package/dist/index/relationships.d.ts +10 -0
- package/dist/index/relationships.d.ts.map +1 -0
- package/dist/index/relationships.js +24 -0
- package/dist/index/relationships.js.map +1 -0
- package/dist/index/usage.d.ts +11 -0
- package/dist/index/usage.d.ts.map +1 -0
- package/dist/index/usage.js +25 -0
- package/dist/index/usage.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/model/build.d.ts +9 -0
- package/dist/model/build.d.ts.map +1 -0
- package/dist/model/build.js +282 -0
- package/dist/model/build.js.map +1 -0
- package/dist/model/names.d.ts +12 -0
- package/dist/model/names.d.ts.map +1 -0
- package/dist/model/names.js +21 -0
- package/dist/model/names.js.map +1 -0
- package/dist/model/types.d.ts +150 -0
- package/dist/model/types.d.ts.map +1 -0
- package/dist/model/types.js +2 -0
- package/dist/model/types.js.map +1 -0
- package/dist/rules/helpers.d.ts +54 -0
- package/dist/rules/helpers.d.ts.map +1 -0
- package/dist/rules/helpers.js +189 -0
- package/dist/rules/helpers.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +4 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/microsoft-bpa/bpa-rules.data.d.ts +12 -0
- package/dist/rules/microsoft-bpa/bpa-rules.data.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/bpa-rules.data.js +664 -0
- package/dist/rules/microsoft-bpa/bpa-rules.data.js.map +1 -0
- package/dist/rules/microsoft-bpa/columns.d.ts +14 -0
- package/dist/rules/microsoft-bpa/columns.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/columns.js +117 -0
- package/dist/rules/microsoft-bpa/columns.js.map +1 -0
- package/dist/rules/microsoft-bpa/define.d.ts +14 -0
- package/dist/rules/microsoft-bpa/define.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/define.js +75 -0
- package/dist/rules/microsoft-bpa/define.js.map +1 -0
- package/dist/rules/microsoft-bpa/dependencies.d.ts +11 -0
- package/dist/rules/microsoft-bpa/dependencies.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/dependencies.js +56 -0
- package/dist/rules/microsoft-bpa/dependencies.js.map +1 -0
- package/dist/rules/microsoft-bpa/index.d.ts +4 -0
- package/dist/rules/microsoft-bpa/index.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/index.js +20 -0
- package/dist/rules/microsoft-bpa/index.js.map +1 -0
- package/dist/rules/microsoft-bpa/live-model.d.ts +3 -0
- package/dist/rules/microsoft-bpa/live-model.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/live-model.js +10 -0
- package/dist/rules/microsoft-bpa/live-model.js.map +1 -0
- package/dist/rules/microsoft-bpa/measures.d.ts +15 -0
- package/dist/rules/microsoft-bpa/measures.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/measures.js +69 -0
- package/dist/rules/microsoft-bpa/measures.js.map +1 -0
- package/dist/rules/microsoft-bpa/naming.d.ts +14 -0
- package/dist/rules/microsoft-bpa/naming.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/naming.js +47 -0
- package/dist/rules/microsoft-bpa/naming.js.map +1 -0
- package/dist/rules/microsoft-bpa/relationships.d.ts +14 -0
- package/dist/rules/microsoft-bpa/relationships.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/relationships.js +97 -0
- package/dist/rules/microsoft-bpa/relationships.js.map +1 -0
- package/dist/rules/microsoft-bpa/tables.d.ts +16 -0
- package/dist/rules/microsoft-bpa/tables.d.ts.map +1 -0
- package/dist/rules/microsoft-bpa/tables.js +147 -0
- package/dist/rules/microsoft-bpa/tables.js.map +1 -0
- package/dist/rules/parse-issue.d.ts +4 -0
- package/dist/rules/parse-issue.d.ts.map +1 -0
- package/dist/rules/parse-issue.js +19 -0
- package/dist/rules/parse-issue.js.map +1 -0
- package/dist/rules/rule-summaries.data.d.ts +3 -0
- package/dist/rules/rule-summaries.data.d.ts.map +1 -0
- package/dist/rules/rule-summaries.data.js +78 -0
- package/dist/rules/rule-summaries.data.js.map +1 -0
- package/dist/rules/types.d.ts +42 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +15 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/tmdl/parse.d.ts +7 -0
- package/dist/tmdl/parse.d.ts.map +1 -0
- package/dist/tmdl/parse.js +196 -0
- package/dist/tmdl/parse.js.map +1 -0
- package/dist/tmdl/quote.d.ts +5 -0
- package/dist/tmdl/quote.d.ts.map +1 -0
- package/dist/tmdl/quote.js +15 -0
- package/dist/tmdl/quote.js.map +1 -0
- package/dist/tmdl/types.d.ts +32 -0
- package/dist/tmdl/types.d.ts.map +1 -0
- package/dist/tmdl/types.js +2 -0
- package/dist/tmdl/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +51 -0
- package/src/engine/config.ts +91 -0
- package/src/engine/ignore.ts +18 -0
- package/src/engine/lint.ts +79 -0
- package/src/engine/rank.ts +67 -0
- package/src/engine/run.ts +67 -0
- package/src/format/index.ts +37 -0
- package/src/format/json.ts +22 -0
- package/src/format/markdown.ts +37 -0
- package/src/format/sarif.ts +85 -0
- package/src/format/text.ts +90 -0
- package/src/index/build.ts +18 -0
- package/src/index/references.ts +170 -0
- package/src/index/relationships.ts +33 -0
- package/src/index/usage.ts +33 -0
- package/src/index.ts +80 -0
- package/src/model/build.ts +306 -0
- package/src/model/names.ts +31 -0
- package/src/model/types.ts +165 -0
- package/src/rules/helpers.ts +223 -0
- package/src/rules/index.ts +5 -0
- package/src/rules/microsoft-bpa/bpa-rules.data.ts +782 -0
- package/src/rules/microsoft-bpa/columns.ts +192 -0
- package/src/rules/microsoft-bpa/define.ts +84 -0
- package/src/rules/microsoft-bpa/dependencies.ts +95 -0
- package/src/rules/microsoft-bpa/index.ts +22 -0
- package/src/rules/microsoft-bpa/live-model.ts +10 -0
- package/src/rules/microsoft-bpa/measures.ts +143 -0
- package/src/rules/microsoft-bpa/naming.ts +104 -0
- package/src/rules/microsoft-bpa/relationships.ts +189 -0
- package/src/rules/microsoft-bpa/tables.ts +249 -0
- package/src/rules/parse-issue.ts +23 -0
- package/src/rules/rule-summaries.data.ts +144 -0
- package/src/rules/types.ts +87 -0
- package/src/tmdl/parse.ts +194 -0
- package/src/tmdl/quote.ts +15 -0
- package/src/tmdl/types.ts +34 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { unquoteName } from "../tmdl/quote.js";
|
|
2
|
+
import type { ParsedFile, TmdlNode } from "../tmdl/types.js";
|
|
3
|
+
import type {
|
|
4
|
+
CalculationGroup,
|
|
5
|
+
CalculationItem,
|
|
6
|
+
Column,
|
|
7
|
+
DataSource,
|
|
8
|
+
Hierarchy,
|
|
9
|
+
Level,
|
|
10
|
+
Measure,
|
|
11
|
+
Model,
|
|
12
|
+
Named,
|
|
13
|
+
Partition,
|
|
14
|
+
Perspective,
|
|
15
|
+
Relationship,
|
|
16
|
+
Role,
|
|
17
|
+
SourceLocation,
|
|
18
|
+
Table,
|
|
19
|
+
TablePermission,
|
|
20
|
+
Variation,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
const str = (v: string | true | undefined): string | undefined =>
|
|
24
|
+
typeof v === "string" ? v : undefined;
|
|
25
|
+
const flag = (v: string | true | undefined): boolean =>
|
|
26
|
+
v === true || (typeof v === "string" && v.toLowerCase() === "true");
|
|
27
|
+
const lower = (v: string | true | undefined): string | undefined => str(v)?.toLowerCase();
|
|
28
|
+
const loc = (n: TmdlNode): SourceLocation => ({ file: n.file, line: n.line });
|
|
29
|
+
const objects = (n: TmdlNode, type: string): TmdlNode[] =>
|
|
30
|
+
n.children.filter((c) => c.kind === "object" && c.type === type);
|
|
31
|
+
|
|
32
|
+
function annotationsOf(n: TmdlNode): Record<string, string> {
|
|
33
|
+
const out: Record<string, string> = {};
|
|
34
|
+
for (const c of objects(n, "annotation")) out[c.name!] = c.value ?? "";
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function named(n: TmdlNode, name = n.name ?? ""): Named {
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
description: n.description,
|
|
42
|
+
annotations: annotationsOf(n),
|
|
43
|
+
location: loc(n),
|
|
44
|
+
node: n,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Split `Table.Column` or `'Some Table'.'Some Column'` into its unquoted parts. */
|
|
49
|
+
export function splitQualifiedName(ref: string): { table: string; column: string } {
|
|
50
|
+
const m = /^('(?:[^']|'')*'|[^.]+)\.('(?:[^']|'')*'|.+)$/.exec(ref.trim());
|
|
51
|
+
return m
|
|
52
|
+
? { table: unquoteName(m[1]!), column: unquoteName(m[2]!) }
|
|
53
|
+
: { table: "", column: unquoteName(ref) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildColumn(c: TmdlNode, table: Table): Column {
|
|
57
|
+
const p = c.props;
|
|
58
|
+
const variations: Variation[] = objects(c, "variation").map((v) => {
|
|
59
|
+
const dc = str(v.props.defaultcolumn);
|
|
60
|
+
return {
|
|
61
|
+
name: v.name!,
|
|
62
|
+
relationship: str(v.props.relationship),
|
|
63
|
+
defaultHierarchy: str(v.props.defaulthierarchy),
|
|
64
|
+
defaultColumn: dc ? splitQualifiedName(dc) : undefined,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const sortBy = str(p.sortbycolumn);
|
|
68
|
+
return {
|
|
69
|
+
...named(c),
|
|
70
|
+
table,
|
|
71
|
+
kind: "data",
|
|
72
|
+
dataType: str(p.datatype),
|
|
73
|
+
isHidden: flag(p.ishidden),
|
|
74
|
+
isKey: flag(p.iskey),
|
|
75
|
+
isAvailableInMdx: p.isavailableinmdx === undefined ? true : flag(p.isavailableinmdx),
|
|
76
|
+
formatString: str(p.formatstring),
|
|
77
|
+
summarizeBy: str(p.summarizeby),
|
|
78
|
+
sourceColumn: str(p.sourcecolumn),
|
|
79
|
+
sortByColumn: sortBy === undefined ? undefined : unquoteName(sortBy),
|
|
80
|
+
dataCategory: str(p.datacategory),
|
|
81
|
+
expression: c.value,
|
|
82
|
+
variations,
|
|
83
|
+
hasAlternateOf: c.children.some((ch) => ch.type === "alternateof"),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildMeasure(x: TmdlNode, table: Table): Measure {
|
|
88
|
+
const p = x.props;
|
|
89
|
+
return {
|
|
90
|
+
...named(x),
|
|
91
|
+
table,
|
|
92
|
+
expression: x.value ?? "",
|
|
93
|
+
formatString: str(p.formatstring),
|
|
94
|
+
formatStringDefinition: str(p.formatstringdefinition),
|
|
95
|
+
isHidden: flag(p.ishidden),
|
|
96
|
+
displayFolder: str(p.displayfolder),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildPartition(pt: TmdlNode, table: Table): Partition {
|
|
101
|
+
const sourceNode = pt.children.find((ch) => ch.type === "source" && ch.kind === "flag");
|
|
102
|
+
const ds = str(sourceNode?.props.datasource);
|
|
103
|
+
return {
|
|
104
|
+
...named(pt),
|
|
105
|
+
table,
|
|
106
|
+
sourceType: (pt.value ?? "").trim().toLowerCase(),
|
|
107
|
+
mode: lower(pt.props.mode),
|
|
108
|
+
source: str(pt.props.source) ?? str(sourceNode?.props.query),
|
|
109
|
+
dataSource: ds === undefined ? undefined : unquoteName(ds),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildHierarchy(h: TmdlNode, table: Table): Hierarchy {
|
|
114
|
+
const hierarchy: Hierarchy = { ...named(h), table, isHidden: flag(h.props.ishidden), levels: [] };
|
|
115
|
+
for (const l of objects(h, "level")) {
|
|
116
|
+
const col = str(l.props.column);
|
|
117
|
+
const level: Level = {
|
|
118
|
+
...named(l),
|
|
119
|
+
hierarchy,
|
|
120
|
+
column: col === undefined ? undefined : unquoteName(col),
|
|
121
|
+
};
|
|
122
|
+
hierarchy.levels.push(level);
|
|
123
|
+
}
|
|
124
|
+
return hierarchy;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildCalculationGroup(cg: TmdlNode, table: Table): CalculationGroup {
|
|
128
|
+
const precedence = str(cg.props.precedence);
|
|
129
|
+
const group: CalculationGroup = {
|
|
130
|
+
...named(cg, table.name),
|
|
131
|
+
table,
|
|
132
|
+
precedence: precedence === undefined ? undefined : Number(precedence),
|
|
133
|
+
items: [],
|
|
134
|
+
};
|
|
135
|
+
for (const ci of objects(cg, "calculationitem")) {
|
|
136
|
+
const item: CalculationItem = {
|
|
137
|
+
...named(ci),
|
|
138
|
+
table,
|
|
139
|
+
expression: ci.value ?? "",
|
|
140
|
+
formatStringDefinition: str(ci.props.formatstringdefinition),
|
|
141
|
+
};
|
|
142
|
+
group.items.push(item);
|
|
143
|
+
}
|
|
144
|
+
return group;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildTable(r: TmdlNode, model: Model): void {
|
|
148
|
+
let t = model.tables.find((x) => x.name === r.name);
|
|
149
|
+
if (!t) {
|
|
150
|
+
t = {
|
|
151
|
+
...named(r),
|
|
152
|
+
kind: "table",
|
|
153
|
+
isHidden: flag(r.props.ishidden),
|
|
154
|
+
dataCategory: str(r.props.datacategory),
|
|
155
|
+
columns: [],
|
|
156
|
+
measures: [],
|
|
157
|
+
partitions: [],
|
|
158
|
+
hierarchies: [],
|
|
159
|
+
};
|
|
160
|
+
model.tables.push(t);
|
|
161
|
+
} else {
|
|
162
|
+
// Partial declaration of an existing table: merge flags and fill blanks.
|
|
163
|
+
if (flag(r.props.ishidden)) t.isHidden = true;
|
|
164
|
+
t.dataCategory ??= str(r.props.datacategory);
|
|
165
|
+
t.description ??= r.description;
|
|
166
|
+
Object.assign(t.annotations, annotationsOf(r));
|
|
167
|
+
}
|
|
168
|
+
for (const c of r.children) {
|
|
169
|
+
if (c.kind === "object" && c.type === "column") t.columns.push(buildColumn(c, t));
|
|
170
|
+
else if (c.kind === "object" && c.type === "measure") t.measures.push(buildMeasure(c, t));
|
|
171
|
+
else if (c.kind === "object" && c.type === "partition") t.partitions.push(buildPartition(c, t));
|
|
172
|
+
else if (c.kind === "object" && c.type === "hierarchy")
|
|
173
|
+
t.hierarchies.push(buildHierarchy(c, t));
|
|
174
|
+
else if (c.type === "calculationgroup") t.calculationGroup = buildCalculationGroup(c, t);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildRelationship(r: TmdlNode): Relationship {
|
|
179
|
+
const p = r.props;
|
|
180
|
+
const from = splitQualifiedName(str(p.fromcolumn) ?? "");
|
|
181
|
+
const to = splitQualifiedName(str(p.tocolumn) ?? "");
|
|
182
|
+
return {
|
|
183
|
+
...named(r),
|
|
184
|
+
fromTable: from.table,
|
|
185
|
+
fromColumn: from.column,
|
|
186
|
+
toTable: to.table,
|
|
187
|
+
toColumn: to.column,
|
|
188
|
+
isActive: p.isactive === undefined ? true : flag(p.isactive),
|
|
189
|
+
crossFilteringBehavior: lower(p.crossfilteringbehavior) ?? "onedirection",
|
|
190
|
+
fromCardinality: lower(p.fromcardinality) ?? "many",
|
|
191
|
+
toCardinality: lower(p.tocardinality) ?? "one",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildRole(r: TmdlNode): Role {
|
|
196
|
+
const role: Role = {
|
|
197
|
+
...named(r),
|
|
198
|
+
modelPermission: lower(r.props.modelpermission),
|
|
199
|
+
members: r.children
|
|
200
|
+
.filter((c) => c.kind === "object" && c.type.endsWith("member"))
|
|
201
|
+
.map((c) => ({ name: c.name! })),
|
|
202
|
+
tablePermissions: [],
|
|
203
|
+
};
|
|
204
|
+
for (const tp of objects(r, "tablepermission")) {
|
|
205
|
+
const permission: TablePermission = {
|
|
206
|
+
...named(tp),
|
|
207
|
+
role,
|
|
208
|
+
table: tp.name!,
|
|
209
|
+
filter: tp.value,
|
|
210
|
+
metadataPermission: lower(tp.props.metadatapermission),
|
|
211
|
+
columnPermissions: objects(tp, "columnpermission").map((cp) => ({
|
|
212
|
+
column: cp.name!,
|
|
213
|
+
permission: (cp.value ?? "").trim().toLowerCase(),
|
|
214
|
+
})),
|
|
215
|
+
};
|
|
216
|
+
role.tablePermissions.push(permission);
|
|
217
|
+
}
|
|
218
|
+
return role;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function finalizeKinds(model: Model): void {
|
|
222
|
+
for (const t of model.tables) {
|
|
223
|
+
if (t.calculationGroup) t.kind = "calculationGroup";
|
|
224
|
+
else if (t.partitions.some((p) => p.sourceType === "calculated")) t.kind = "calculated";
|
|
225
|
+
else t.kind = "table";
|
|
226
|
+
for (const c of t.columns)
|
|
227
|
+
c.kind =
|
|
228
|
+
c.expression !== undefined
|
|
229
|
+
? "calculated"
|
|
230
|
+
: t.kind === "calculated"
|
|
231
|
+
? "calculatedTable"
|
|
232
|
+
: "data";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildModel(files: ParsedFile[]): Model {
|
|
237
|
+
const model: Model = {
|
|
238
|
+
name: "Model",
|
|
239
|
+
annotations: {},
|
|
240
|
+
location: { file: "", line: 0 },
|
|
241
|
+
props: {},
|
|
242
|
+
tables: [],
|
|
243
|
+
relationships: [],
|
|
244
|
+
roles: [],
|
|
245
|
+
perspectives: [],
|
|
246
|
+
cultures: [],
|
|
247
|
+
expressions: [],
|
|
248
|
+
functions: [],
|
|
249
|
+
dataSources: [],
|
|
250
|
+
files,
|
|
251
|
+
};
|
|
252
|
+
for (const f of files) {
|
|
253
|
+
for (const r of f.roots) {
|
|
254
|
+
if (r.kind === "ref" || r.kind === "prop" || r.kind === "expr") continue;
|
|
255
|
+
switch (r.type) {
|
|
256
|
+
case "model":
|
|
257
|
+
Object.assign(model, named(r, r.name ?? "Model"), {
|
|
258
|
+
annotations: { ...model.annotations, ...annotationsOf(r) },
|
|
259
|
+
props: r.props,
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
case "annotation":
|
|
263
|
+
if (r.name) model.annotations[r.name] = r.value ?? "";
|
|
264
|
+
break;
|
|
265
|
+
case "table":
|
|
266
|
+
buildTable(r, model);
|
|
267
|
+
break;
|
|
268
|
+
case "relationship":
|
|
269
|
+
model.relationships.push(buildRelationship(r));
|
|
270
|
+
break;
|
|
271
|
+
case "role":
|
|
272
|
+
model.roles.push(buildRole(r));
|
|
273
|
+
break;
|
|
274
|
+
case "perspective": {
|
|
275
|
+
const p: Perspective = {
|
|
276
|
+
...named(r),
|
|
277
|
+
tables: objects(r, "perspectivetable").map((t) => t.name!),
|
|
278
|
+
};
|
|
279
|
+
model.perspectives.push(p);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "cultureinfo":
|
|
283
|
+
model.cultures.push(named(r));
|
|
284
|
+
break;
|
|
285
|
+
case "expression":
|
|
286
|
+
model.expressions.push({ ...named(r), expression: r.value ?? "" });
|
|
287
|
+
break;
|
|
288
|
+
case "function":
|
|
289
|
+
model.functions.push({ ...named(r), expression: r.value ?? "" });
|
|
290
|
+
break;
|
|
291
|
+
case "datasource": {
|
|
292
|
+
const ds: DataSource = {
|
|
293
|
+
...named(r),
|
|
294
|
+
kind: (r.value ?? "").trim().toLowerCase() === "provider" ? "provider" : "structured",
|
|
295
|
+
};
|
|
296
|
+
model.dataSources.push(ds);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
default:
|
|
300
|
+
break; // database, queryGroup, extendedProperty, unknown: kept in files, not modeled
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
finalizeKinds(model);
|
|
305
|
+
return model;
|
|
306
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Relationship } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** `'Name'` with embedded single quotes doubled, as DAX and Tabular Editor write table names. */
|
|
4
|
+
export const tableRef = (name: string): string => `'${name.replace(/'/g, "''")}'`;
|
|
5
|
+
|
|
6
|
+
const bracket = (name: string): string => `[${name.replace(/\]/g, "]]")}]`;
|
|
7
|
+
|
|
8
|
+
export const columnRef = (table: string, column: string): string =>
|
|
9
|
+
`${tableRef(table)}${bracket(column)}`;
|
|
10
|
+
|
|
11
|
+
export const measureRef = (name: string): string => bracket(name);
|
|
12
|
+
|
|
13
|
+
const cardinalitySymbol = (c: string): string => (c === "many" ? "∞" : c === "one" ? "1" : "?");
|
|
14
|
+
|
|
15
|
+
/** Tabular Editor's relationship display name, e.g. `'Sales'[Sale Date] ∞←1 'Date'[Date]`. */
|
|
16
|
+
export function relationshipName(r: Relationship): string {
|
|
17
|
+
const arrow = r.crossFilteringBehavior === "bothdirections" ? "↔" : "←";
|
|
18
|
+
return `${columnRef(r.fromTable, r.fromColumn)} ${cardinalitySymbol(r.fromCardinality)}${arrow}${cardinalitySymbol(r.toCardinality)} ${columnRef(r.toTable, r.toColumn)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Rule id to page slug: lowercase, runs of non-alphanumerics become one dash, no leading or trailing dash. */
|
|
22
|
+
export function slug(id: string): string {
|
|
23
|
+
return id
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
26
|
+
.replace(/^-+|-+$/g, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const RULE_URL_BASE = "https://pbiplint.com/rules/";
|
|
30
|
+
|
|
31
|
+
export const ruleUrl = (id: string): string => RULE_URL_BASE + slug(id);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { ParsedFile, TmdlNode } from "../tmdl/types.js";
|
|
2
|
+
|
|
3
|
+
export interface SourceLocation {
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Fields shared by every model object. */
|
|
9
|
+
export interface Named {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Child `annotation` nodes by name. */
|
|
13
|
+
annotations: Record<string, string>;
|
|
14
|
+
location: SourceLocation;
|
|
15
|
+
node?: TmdlNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TableKind = "table" | "calculated" | "calculationGroup";
|
|
19
|
+
export type ColumnKind = "data" | "calculated" | "calculatedTable";
|
|
20
|
+
|
|
21
|
+
export interface Model extends Named {
|
|
22
|
+
/** Properties declared under `model`, keys lowercased. */
|
|
23
|
+
props: Record<string, string | true>;
|
|
24
|
+
tables: Table[];
|
|
25
|
+
relationships: Relationship[];
|
|
26
|
+
roles: Role[];
|
|
27
|
+
perspectives: Perspective[];
|
|
28
|
+
cultures: Culture[];
|
|
29
|
+
expressions: NamedExpression[];
|
|
30
|
+
functions: DaxFunction[];
|
|
31
|
+
dataSources: DataSource[];
|
|
32
|
+
files: ParsedFile[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Table extends Named {
|
|
36
|
+
kind: TableKind;
|
|
37
|
+
isHidden: boolean;
|
|
38
|
+
dataCategory?: string;
|
|
39
|
+
columns: Column[];
|
|
40
|
+
measures: Measure[];
|
|
41
|
+
partitions: Partition[];
|
|
42
|
+
hierarchies: Hierarchy[];
|
|
43
|
+
calculationGroup?: CalculationGroup;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Variation {
|
|
47
|
+
name: string;
|
|
48
|
+
relationship?: string;
|
|
49
|
+
defaultHierarchy?: string;
|
|
50
|
+
defaultColumn?: { table: string; column: string };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Column extends Named {
|
|
54
|
+
table: Table;
|
|
55
|
+
kind: ColumnKind;
|
|
56
|
+
dataType?: string;
|
|
57
|
+
isHidden: boolean;
|
|
58
|
+
isKey: boolean;
|
|
59
|
+
/** TOM default is true; TMDL only writes `isAvailableInMdx: false`. */
|
|
60
|
+
isAvailableInMdx: boolean;
|
|
61
|
+
formatString?: string;
|
|
62
|
+
summarizeBy?: string;
|
|
63
|
+
sourceColumn?: string;
|
|
64
|
+
sortByColumn?: string;
|
|
65
|
+
dataCategory?: string;
|
|
66
|
+
expression?: string;
|
|
67
|
+
variations: Variation[];
|
|
68
|
+
hasAlternateOf: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Measure extends Named {
|
|
72
|
+
table: Table;
|
|
73
|
+
expression: string;
|
|
74
|
+
formatString?: string;
|
|
75
|
+
/** Raw DAX of `formatStringDefinition = ...`, quotes included. */
|
|
76
|
+
formatStringDefinition?: string;
|
|
77
|
+
isHidden: boolean;
|
|
78
|
+
displayFolder?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface Partition extends Named {
|
|
82
|
+
table: Table;
|
|
83
|
+
/** Lowercased word after `=` in the header: m, calculated, query, calculationgroup, entity, ... */
|
|
84
|
+
sourceType: string;
|
|
85
|
+
mode?: string;
|
|
86
|
+
/** M, DAX, or native query text. */
|
|
87
|
+
source?: string;
|
|
88
|
+
dataSource?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface Hierarchy extends Named {
|
|
92
|
+
table: Table;
|
|
93
|
+
isHidden: boolean;
|
|
94
|
+
levels: Level[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Level extends Named {
|
|
98
|
+
hierarchy: Hierarchy;
|
|
99
|
+
column?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CalculationGroup extends Named {
|
|
103
|
+
table: Table;
|
|
104
|
+
precedence?: number;
|
|
105
|
+
items: CalculationItem[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CalculationItem extends Named {
|
|
109
|
+
table: Table;
|
|
110
|
+
expression: string;
|
|
111
|
+
formatStringDefinition?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface Relationship extends Named {
|
|
115
|
+
fromTable: string;
|
|
116
|
+
fromColumn: string;
|
|
117
|
+
toTable: string;
|
|
118
|
+
toColumn: string;
|
|
119
|
+
isActive: boolean;
|
|
120
|
+
/** Lowercased: onedirection | bothdirections | automatic */
|
|
121
|
+
crossFilteringBehavior: string;
|
|
122
|
+
/** Lowercased: many | one | none */
|
|
123
|
+
fromCardinality: string;
|
|
124
|
+
toCardinality: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RoleMember {
|
|
128
|
+
name: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface TablePermission extends Named {
|
|
132
|
+
role: Role;
|
|
133
|
+
/** Same as `name`: the table the permission applies to. */
|
|
134
|
+
table: string;
|
|
135
|
+
/** DAX filter expression; undefined when the permission declares none. */
|
|
136
|
+
filter?: string;
|
|
137
|
+
/** Lowercased, for object-level security (`none`, `default`, ...). */
|
|
138
|
+
metadataPermission?: string;
|
|
139
|
+
columnPermissions: { column: string; permission: string }[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface Role extends Named {
|
|
143
|
+
modelPermission?: string;
|
|
144
|
+
members: RoleMember[];
|
|
145
|
+
tablePermissions: TablePermission[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface Perspective extends Named {
|
|
149
|
+
/** Names of `perspectiveTable` children. */
|
|
150
|
+
tables: string[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type Culture = Named;
|
|
154
|
+
|
|
155
|
+
export interface NamedExpression extends Named {
|
|
156
|
+
expression: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface DaxFunction extends Named {
|
|
160
|
+
expression: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface DataSource extends Named {
|
|
164
|
+
kind: "provider" | "structured";
|
|
165
|
+
}
|