@xdbml/parse 0.3.2 → 0.5.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/ast.d.ts +24 -8
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/keywords.d.ts +3 -3
- package/dist/keywords.js +14 -0
- package/dist/module-resolver.d.ts +3 -3
- package/dist/module-resolver.js +21 -11
- package/dist/name-resolver.d.ts +2 -2
- package/dist/name-resolver.js +43 -3
- package/dist/parser.d.ts +31 -7
- package/dist/parser.js +116 -18
- package/dist/relationships.d.ts +99 -0
- package/dist/relationships.js +436 -0
- package/dist/supertypes.d.ts +110 -0
- package/dist/supertypes.js +526 -0
- package/package.json +2 -1
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relationship helpers and checks (spec 11.10 through 11.12, new in v0.4).
|
|
3
|
+
*
|
|
4
|
+
* A `Ref` carries its relationship type in its settings block. The
|
|
5
|
+
* `foreign_master` flag marks denormalized replication: the parent endpoint
|
|
6
|
+
* holds the master value for a copy held at the child endpoint. Absence of
|
|
7
|
+
* the flag marks a referential relationship, the foreign key.
|
|
8
|
+
*
|
|
9
|
+
* This module holds three things:
|
|
10
|
+
*
|
|
11
|
+
* 1. `relationshipType()` and `isForeignMaster()` -- one definition of the
|
|
12
|
+
* type test, so the renderer, the MCP server, and any generator agree
|
|
13
|
+
* rather than each re-reading the settings array.
|
|
14
|
+
*
|
|
15
|
+
* 2. `refChildEndpoint()` / `refParentEndpoint()` -- which side of a Ref is
|
|
16
|
+
* the child. The cardinality operator decides: `>` and `-` put the child
|
|
17
|
+
* on the left, `<` puts it on the right. `<>` has no single child.
|
|
18
|
+
*
|
|
19
|
+
* 3. `checkRelationships()` -- the semantic rules of spec 11.11 that the
|
|
20
|
+
* grammar cannot express. Called from `resolveNames()`, so every
|
|
21
|
+
* consumer of the resolver's diagnostics gets them without opting in.
|
|
22
|
+
*/
|
|
23
|
+
/** The flag name, spelled once. */
|
|
24
|
+
export const FOREIGN_MASTER_FLAG = 'foreign_master';
|
|
25
|
+
/** Accepted values of the `constraint_type` setting (spec 11.15). */
|
|
26
|
+
export const CONSTRAINT_TYPES = ['identifying', 'non_identifying'];
|
|
27
|
+
/**
|
|
28
|
+
* Relationship settings introduced in v0.4 alongside the foreign master
|
|
29
|
+
* flag. Gated on the declared version like the flag itself, so a document
|
|
30
|
+
* declaring an earlier version does not quietly carry v0.4 semantics.
|
|
31
|
+
*/
|
|
32
|
+
export const V04_RELATIONSHIP_SETTINGS = [
|
|
33
|
+
'source_role',
|
|
34
|
+
'target_role',
|
|
35
|
+
'source_verb',
|
|
36
|
+
'target_verb',
|
|
37
|
+
'constraint_type',
|
|
38
|
+
];
|
|
39
|
+
/** The `constraint_type` a Ref declares, or undefined when unstated. */
|
|
40
|
+
export function constraintType(ref) {
|
|
41
|
+
const s = ref.settings.find((x) => x.name === 'constraint_type');
|
|
42
|
+
if (!s || !s.value)
|
|
43
|
+
return undefined;
|
|
44
|
+
const raw = settingText(s);
|
|
45
|
+
return CONSTRAINT_TYPES.includes(raw)
|
|
46
|
+
? raw
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
/** True when the Ref is marked `undirected: true` (spec 11.16.2). */
|
|
50
|
+
export function isUndirected(ref) {
|
|
51
|
+
const s = ref.settings.find((x) => x.name === 'undirected');
|
|
52
|
+
return !!s && !!s.value && settingText(s) === 'true';
|
|
53
|
+
}
|
|
54
|
+
/** The text of a setting value, for the string-ish value kinds. */
|
|
55
|
+
function settingText(s) {
|
|
56
|
+
const v = s.value;
|
|
57
|
+
if (!v)
|
|
58
|
+
return '';
|
|
59
|
+
switch (v.kind) {
|
|
60
|
+
case 'StringValue':
|
|
61
|
+
case 'IdentifierValue':
|
|
62
|
+
case 'NumberValue':
|
|
63
|
+
return String(v.value);
|
|
64
|
+
case 'BooleanValue':
|
|
65
|
+
return String(v.value);
|
|
66
|
+
default:
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True when a settings array carries the `foreign_master` flag. The flag
|
|
72
|
+
* takes no value, so presence is the whole test; a `foreign_master: true`
|
|
73
|
+
* spelling is accepted as well rather than silently ignored, since a user
|
|
74
|
+
* reaching for the more explicit form means the same thing.
|
|
75
|
+
*/
|
|
76
|
+
export function hasForeignMasterFlag(settings) {
|
|
77
|
+
return settings.some((s) => s.name === FOREIGN_MASTER_FLAG);
|
|
78
|
+
}
|
|
79
|
+
/** The relationship type a Ref declares. */
|
|
80
|
+
export function relationshipType(ref) {
|
|
81
|
+
return hasForeignMasterFlag(ref.settings) ? 'foreign_master' : 'referential';
|
|
82
|
+
}
|
|
83
|
+
/** Convenience wrapper over `relationshipType`. */
|
|
84
|
+
export function isForeignMaster(ref) {
|
|
85
|
+
return relationshipType(ref) === 'foreign_master';
|
|
86
|
+
}
|
|
87
|
+
/* -------------------------------------------------------------------------
|
|
88
|
+
* Which end is the child
|
|
89
|
+
* ----------------------------------------------------------------------- */
|
|
90
|
+
/**
|
|
91
|
+
* The child endpoint of a Ref, i.e. the side holding the copy (for a foreign
|
|
92
|
+
* master) or the foreign key (for a referential relationship).
|
|
93
|
+
*
|
|
94
|
+
* a.x > b.y many a to one b -> a.x is the child
|
|
95
|
+
* a.x < b.y one a to many b -> b.y is the child
|
|
96
|
+
* a.x - b.y one to one -> a.x is the child, by convention
|
|
97
|
+
* a.x <> b.y many to many -> no single child; returns undefined
|
|
98
|
+
*/
|
|
99
|
+
export function refChildEndpoint(ref) {
|
|
100
|
+
switch (ref.spec.operator) {
|
|
101
|
+
case '>':
|
|
102
|
+
case '-':
|
|
103
|
+
return ref.spec.source;
|
|
104
|
+
case '<':
|
|
105
|
+
return ref.spec.target;
|
|
106
|
+
default:
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The parent endpoint of a Ref, i.e. the opposite side of the child. */
|
|
111
|
+
export function refParentEndpoint(ref) {
|
|
112
|
+
switch (ref.spec.operator) {
|
|
113
|
+
case '>':
|
|
114
|
+
case '-':
|
|
115
|
+
return ref.spec.target;
|
|
116
|
+
case '<':
|
|
117
|
+
return ref.spec.source;
|
|
118
|
+
default:
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Every name an entity answers to in a document: its own name, and its
|
|
124
|
+
* container-qualified name where it sits inside one. Mirrors the index the
|
|
125
|
+
* renderer builds, so both agree on what an entity-level path looks like.
|
|
126
|
+
*/
|
|
127
|
+
export function entityNames(doc) {
|
|
128
|
+
const names = new Set();
|
|
129
|
+
const add = (name, container) => {
|
|
130
|
+
names.add(name);
|
|
131
|
+
if (container)
|
|
132
|
+
names.add(`${container}.${name}`);
|
|
133
|
+
};
|
|
134
|
+
for (const stmt of doc.statements) {
|
|
135
|
+
if (stmt.kind === 'EntityDeclaration')
|
|
136
|
+
add(stmt.name);
|
|
137
|
+
else if (stmt.kind === 'ContainerDeclaration') {
|
|
138
|
+
for (const item of stmt.body) {
|
|
139
|
+
if (item.kind === 'EntityDeclaration')
|
|
140
|
+
add(item.name, stmt.name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return names;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* True when an endpoint names an entity and stops there (spec 11.16), as in
|
|
148
|
+
* `Customer` or `shop.orders`. A structural test is not enough: `b.aid` has
|
|
149
|
+
* the same shape and names a field, so the whole path is checked against the
|
|
150
|
+
* entities the document declares.
|
|
151
|
+
*/
|
|
152
|
+
export function isEntityLevelEndpoint(endpoint, names) {
|
|
153
|
+
if (endpoint.compositeFields && endpoint.compositeFields.length > 0)
|
|
154
|
+
return false;
|
|
155
|
+
if (!endpoint.path.every((seg) => seg.kind === 'PathField'))
|
|
156
|
+
return false;
|
|
157
|
+
return names.has(pathToString(endpoint.path));
|
|
158
|
+
}
|
|
159
|
+
/** Render a path as a dotted string, for messages and for identity keys. */
|
|
160
|
+
export function pathToString(path) {
|
|
161
|
+
return path
|
|
162
|
+
.map((seg) => {
|
|
163
|
+
switch (seg.kind) {
|
|
164
|
+
case 'PathField': return seg.name;
|
|
165
|
+
case 'PathArrayIndex': return `[${seg.index}]`;
|
|
166
|
+
case 'PathArrayWildcard': return '[*]';
|
|
167
|
+
case 'PathMapKey': return `[${seg.key}]`;
|
|
168
|
+
default: return '?';
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.join('.');
|
|
172
|
+
}
|
|
173
|
+
/* -------------------------------------------------------------------------
|
|
174
|
+
* Version gating
|
|
175
|
+
* ----------------------------------------------------------------------- */
|
|
176
|
+
/**
|
|
177
|
+
* True when the document declares at least `min`. A document with no version
|
|
178
|
+
* declaration is plain DBML and supports no xDBML construct, so it fails
|
|
179
|
+
* every gate.
|
|
180
|
+
*
|
|
181
|
+
* Kept general rather than special-cased to the foreign master flag: the
|
|
182
|
+
* module-system gates described in the grammar notes are not implemented yet
|
|
183
|
+
* and can adopt this helper when they are.
|
|
184
|
+
*/
|
|
185
|
+
export function versionAtLeast(doc, min) {
|
|
186
|
+
if (!doc.version)
|
|
187
|
+
return false;
|
|
188
|
+
const parse = (v) => v.split('.').map((n) => Number(n) || 0);
|
|
189
|
+
const have = parse(doc.version.version);
|
|
190
|
+
const want = parse(min);
|
|
191
|
+
for (let i = 0; i < Math.max(have.length, want.length); i++) {
|
|
192
|
+
const a = have[i] ?? 0;
|
|
193
|
+
const b = want[i] ?? 0;
|
|
194
|
+
if (a !== b)
|
|
195
|
+
return a > b;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/* -------------------------------------------------------------------------
|
|
200
|
+
* Checks
|
|
201
|
+
* ----------------------------------------------------------------------- */
|
|
202
|
+
/**
|
|
203
|
+
* Semantic checks for foreign master relationships (spec 11.11), plus the
|
|
204
|
+
* inline-form rule of 11.10.2 and the version gate of section 4.
|
|
205
|
+
*
|
|
206
|
+
* The composite rule is checked here rather than in the grammar so the error
|
|
207
|
+
* can name the flag that makes the composite invalid, instead of failing on
|
|
208
|
+
* a form that is perfectly legal for a referential relationship.
|
|
209
|
+
*/
|
|
210
|
+
export function checkRelationships(doc) {
|
|
211
|
+
const diagnostics = [];
|
|
212
|
+
// Child endpoints already claimed by a foreign master, so a second one
|
|
213
|
+
// into the same attribute can be reported. Keyed by the dotted path.
|
|
214
|
+
const claimedChildren = new Map();
|
|
215
|
+
let sawForeignMaster = false;
|
|
216
|
+
const noteForeignMaster = () => { sawForeignMaster = true; };
|
|
217
|
+
/* ---- top-level and container-level Ref declarations ---- */
|
|
218
|
+
const declaredEntities = entityNames(doc);
|
|
219
|
+
// Any v0.4 relationship setting present anywhere in the document, so the
|
|
220
|
+
// version gate below covers the documentation settings as well as the flag.
|
|
221
|
+
let sawV04Setting = false;
|
|
222
|
+
const checkRelationshipSettings = (ref) => {
|
|
223
|
+
const entityLevel = isEntityLevelEndpoint(ref.spec.source, declaredEntities)
|
|
224
|
+
&& isEntityLevelEndpoint(ref.spec.target, declaredEntities);
|
|
225
|
+
for (const setting of ref.settings) {
|
|
226
|
+
if (V04_RELATIONSHIP_SETTINGS.includes(setting.name)) {
|
|
227
|
+
sawV04Setting = true;
|
|
228
|
+
}
|
|
229
|
+
// 11.15: the value vocabulary is closed, so a typo is an error rather
|
|
230
|
+
// than a silently ignored setting.
|
|
231
|
+
if (setting.name === 'constraint_type') {
|
|
232
|
+
const raw = settingText(setting);
|
|
233
|
+
if (!CONSTRAINT_TYPES.includes(raw)) {
|
|
234
|
+
diagnostics.push({
|
|
235
|
+
severity: 'error',
|
|
236
|
+
code: 'invalid-constraint-type',
|
|
237
|
+
message: `'${raw || '(no value)'}' is not a constraint type. ` +
|
|
238
|
+
`Use ${CONSTRAINT_TYPES.map((v) => `'${v}'`).join(' or ')}.`,
|
|
239
|
+
span: setting.span,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else if (isForeignMaster(ref)) {
|
|
243
|
+
// 11.15: a foreign master carries no key dependency, so there is
|
|
244
|
+
// nothing for identifying or non-identifying to describe.
|
|
245
|
+
diagnostics.push({
|
|
246
|
+
severity: 'error',
|
|
247
|
+
code: 'constraint-type-on-foreign-master',
|
|
248
|
+
message: "'constraint_type' does not apply to a foreign master relationship, which carries no key dependency.",
|
|
249
|
+
span: setting.span,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (setting.name === 'undirected') {
|
|
254
|
+
const raw = settingText(setting);
|
|
255
|
+
if (raw !== 'true' && raw !== 'false') {
|
|
256
|
+
diagnostics.push({
|
|
257
|
+
severity: 'error',
|
|
258
|
+
code: 'invalid-undirected',
|
|
259
|
+
message: `'undirected' takes true or false, not '${raw || '(no value)'}'.`,
|
|
260
|
+
span: setting.span,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// 11.16.2: many-to-many states a cardinality, and an entity-level
|
|
266
|
+
// relationship states none. A many-to-many that carries its own
|
|
267
|
+
// attributes is an Edge.
|
|
268
|
+
if (entityLevel && ref.spec.operator === '<>') {
|
|
269
|
+
diagnostics.push({
|
|
270
|
+
severity: 'error',
|
|
271
|
+
code: 'entity-level-many-to-many',
|
|
272
|
+
message: "'<>' is not available on a relationship between entities: many-to-many states a cardinality, " +
|
|
273
|
+
'which an entity-level relationship leaves unstated. Declare an Edge if the relationship carries its own attributes.',
|
|
274
|
+
span: ref.spec.span ?? ref.span,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const checkRefDeclaration = (ref) => {
|
|
279
|
+
checkRelationshipSettings(ref);
|
|
280
|
+
if (!isForeignMaster(ref))
|
|
281
|
+
return;
|
|
282
|
+
noteForeignMaster();
|
|
283
|
+
// 11.11: neither endpoint may be composite.
|
|
284
|
+
for (const [label, endpoint] of [
|
|
285
|
+
['source', ref.spec.source],
|
|
286
|
+
['target', ref.spec.target],
|
|
287
|
+
]) {
|
|
288
|
+
if (endpoint.compositeFields && endpoint.compositeFields.length > 0) {
|
|
289
|
+
diagnostics.push({
|
|
290
|
+
severity: 'error',
|
|
291
|
+
code: 'foreign-master-composite',
|
|
292
|
+
message: `A foreign master relationship takes a single attribute on each side; the ${label} endpoint is composite. ` +
|
|
293
|
+
'Write one foreign master relationship per duplicated attribute.',
|
|
294
|
+
span: endpoint.span,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// 11.11: at most one master per child attribute.
|
|
299
|
+
const child = refChildEndpoint(ref);
|
|
300
|
+
if (child) {
|
|
301
|
+
const key = pathToString(child.path);
|
|
302
|
+
const previous = claimedChildren.get(key);
|
|
303
|
+
if (previous) {
|
|
304
|
+
diagnostics.push({
|
|
305
|
+
severity: 'error',
|
|
306
|
+
code: 'foreign-master-duplicate-child',
|
|
307
|
+
message: `'${key}' already has a foreign master. A copied attribute has one master, ` +
|
|
308
|
+
'so a child attribute is the child of at most one foreign master relationship.',
|
|
309
|
+
span: child.span,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
claimedChildren.set(key, child.span);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
/* ---- inline refs on fields ---- */
|
|
318
|
+
const checkField = (field, ownerPath) => {
|
|
319
|
+
const flag = field.settings.find((s) => s.name === FOREIGN_MASTER_FLAG);
|
|
320
|
+
if (!flag)
|
|
321
|
+
return;
|
|
322
|
+
noteForeignMaster();
|
|
323
|
+
const inlineRef = field.settings.find((s) => s.value && s.value.kind === 'RefValue');
|
|
324
|
+
// 11.10.2: the flag qualifies an inline ref: and is an error without one.
|
|
325
|
+
if (!inlineRef) {
|
|
326
|
+
diagnostics.push({
|
|
327
|
+
severity: 'error',
|
|
328
|
+
code: 'foreign-master-without-ref',
|
|
329
|
+
message: "The 'foreign_master' flag qualifies an inline 'ref:' in the same settings block; " +
|
|
330
|
+
'this field declares no inline ref.',
|
|
331
|
+
span: flag.span,
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// 11.11: at most one master per child attribute. For an inline ref the
|
|
336
|
+
// child is always the field carrying the setting, whatever the operator.
|
|
337
|
+
const key = `${ownerPath}.${field.name}`;
|
|
338
|
+
const previous = claimedChildren.get(key);
|
|
339
|
+
if (previous) {
|
|
340
|
+
diagnostics.push({
|
|
341
|
+
severity: 'error',
|
|
342
|
+
code: 'foreign-master-duplicate-child',
|
|
343
|
+
message: `'${key}' already has a foreign master. A copied attribute has one master, ` +
|
|
344
|
+
'so a child attribute is the child of at most one foreign master relationship.',
|
|
345
|
+
span: field.span,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
claimedChildren.set(key, field.span);
|
|
350
|
+
}
|
|
351
|
+
// 11.11: an inline foreign master cannot be composite, because the
|
|
352
|
+
// grammar gives an inline ref a single target; nothing to check here.
|
|
353
|
+
};
|
|
354
|
+
/* ---- walk ---- */
|
|
355
|
+
const walkFields = (items, ownerPath) => {
|
|
356
|
+
for (const item of items) {
|
|
357
|
+
if (item.kind !== 'FieldDeclaration')
|
|
358
|
+
continue;
|
|
359
|
+
const field = item;
|
|
360
|
+
checkField(field, ownerPath);
|
|
361
|
+
// Descend into nested object/array bodies so a flag on a nested field
|
|
362
|
+
// is checked too. Nested field containers vary by type expression;
|
|
363
|
+
// `nestedFields` below normalizes the shapes the AST uses.
|
|
364
|
+
for (const nested of nestedFields(field)) {
|
|
365
|
+
walkFields([nested], `${ownerPath}.${field.name}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const walkEntity = (entity, containerName) => {
|
|
370
|
+
const ownerPath = containerName ? `${containerName}.${entity.name}` : entity.name;
|
|
371
|
+
walkFields(entity.body, ownerPath);
|
|
372
|
+
};
|
|
373
|
+
for (const stmt of doc.statements) {
|
|
374
|
+
switch (stmt.kind) {
|
|
375
|
+
case 'RefDeclaration':
|
|
376
|
+
checkRefDeclaration(stmt);
|
|
377
|
+
break;
|
|
378
|
+
case 'EntityDeclaration':
|
|
379
|
+
walkEntity(stmt);
|
|
380
|
+
break;
|
|
381
|
+
case 'ContainerDeclaration': {
|
|
382
|
+
// Ref declarations are top-level only (spec 11.2), so a container
|
|
383
|
+
// body holds entities, views, edges, enums and notes but no refs.
|
|
384
|
+
const container = stmt;
|
|
385
|
+
for (const item of container.body) {
|
|
386
|
+
if (item.kind === 'EntityDeclaration')
|
|
387
|
+
walkEntity(item, container.name);
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
default:
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Section 4: the construct requires a document declaring 0.4 or later.
|
|
396
|
+
if ((sawForeignMaster || sawV04Setting) && !versionAtLeast(doc, '0.4')) {
|
|
397
|
+
const what = sawForeignMaster
|
|
398
|
+
? "The 'foreign_master' relationship flag"
|
|
399
|
+
: 'Relationship roles, verbs and constraint type';
|
|
400
|
+
diagnostics.push({
|
|
401
|
+
severity: 'error',
|
|
402
|
+
code: 'construct-requires-version',
|
|
403
|
+
message: `${what} requires a document declaring 'xdbml: 0.4' or later.`,
|
|
404
|
+
span: doc.version ? doc.version.span : doc.span,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return diagnostics;
|
|
408
|
+
}
|
|
409
|
+
/* -------------------------------------------------------------------------
|
|
410
|
+
* Helpers
|
|
411
|
+
* ----------------------------------------------------------------------- */
|
|
412
|
+
/**
|
|
413
|
+
* The field declarations nested inside a field's type expression, for the
|
|
414
|
+
* object and array shapes the AST produces. Returns an empty array for
|
|
415
|
+
* scalar fields and for type expressions with no inner field list.
|
|
416
|
+
*/
|
|
417
|
+
function nestedFields(field) {
|
|
418
|
+
const out = [];
|
|
419
|
+
const visit = (node) => {
|
|
420
|
+
if (!node || typeof node !== 'object')
|
|
421
|
+
return;
|
|
422
|
+
const rec = node;
|
|
423
|
+
if (rec.kind === 'FieldDeclaration') {
|
|
424
|
+
out.push(rec);
|
|
425
|
+
return; // walkFields recurses into this one itself
|
|
426
|
+
}
|
|
427
|
+
for (const value of Object.values(rec)) {
|
|
428
|
+
if (Array.isArray(value))
|
|
429
|
+
value.forEach(visit);
|
|
430
|
+
else if (value && typeof value === 'object')
|
|
431
|
+
visit(value);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
visit(field.type);
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supertype groups (spec §12, new in v0.5).
|
|
3
|
+
*
|
|
4
|
+
* Three things live here:
|
|
5
|
+
*
|
|
6
|
+
* - Setting normalization. `completeness`, `exclusivity`, `strategy` and
|
|
7
|
+
* `merge` accept a canonical value and a set of aliases (spec §12.2).
|
|
8
|
+
* The AST keeps what was written; `supertypeGroupSettings()` returns
|
|
9
|
+
* the canonical values.
|
|
10
|
+
* - Member resolution. The supertype and every subtype name an entity by
|
|
11
|
+
* path, bare (`Person`) or container-qualified (`crm.Person`), with the
|
|
12
|
+
* same reading as an entity-level `Ref` endpoint (spec §11.16.3).
|
|
13
|
+
* - The validation rules summarized in spec §12.8, run by `resolveNames()`
|
|
14
|
+
* on the flattened document so imported and cloned groups are checked
|
|
15
|
+
* like local ones.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here computes the attributes or keys a subtype would hold in a
|
|
18
|
+
* physical model. A subtype declares its own attributes only; placing the
|
|
19
|
+
* supertype's attributes in stored structures is derivation output (spec
|
|
20
|
+
* §12.5, §12.7). Supertype chains are computed for the structural checks
|
|
21
|
+
* and for tools that display the hierarchy.
|
|
22
|
+
*/
|
|
23
|
+
import type { Diagnostic } from './name-resolver.ts';
|
|
24
|
+
import type { SupertypeGroupDeclaration, SupertypeGroupMember, XDbmlDocument } from './ast.ts';
|
|
25
|
+
export type Completeness = 'total' | 'partial';
|
|
26
|
+
export type Exclusivity = 'disjoint' | 'overlapping';
|
|
27
|
+
export type MaterializationStrategy = 'preserved_hierarchy' | 'roll_up' | 'roll_down';
|
|
28
|
+
export type MergeOption = 'flat' | 'nested';
|
|
29
|
+
/**
|
|
30
|
+
* Accepted spellings, keyed by lowercase spelling, mapped to the canonical
|
|
31
|
+
* value. Canonical values map to themselves.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SUPERTYPE_GROUP_VALUES: {
|
|
34
|
+
readonly completeness: {
|
|
35
|
+
readonly total: "total";
|
|
36
|
+
readonly complete: "total";
|
|
37
|
+
readonly partial: "partial";
|
|
38
|
+
readonly incomplete: "partial";
|
|
39
|
+
};
|
|
40
|
+
readonly exclusivity: {
|
|
41
|
+
readonly disjoint: "disjoint";
|
|
42
|
+
readonly exclusive: "disjoint";
|
|
43
|
+
readonly overlapping: "overlapping";
|
|
44
|
+
readonly non_exclusive: "overlapping";
|
|
45
|
+
};
|
|
46
|
+
readonly strategy: {
|
|
47
|
+
readonly preserved_hierarchy: "preserved_hierarchy";
|
|
48
|
+
readonly class_table: "preserved_hierarchy";
|
|
49
|
+
readonly joined: "preserved_hierarchy";
|
|
50
|
+
readonly roll_up: "roll_up";
|
|
51
|
+
readonly single_table: "roll_up";
|
|
52
|
+
readonly roll_down: "roll_down";
|
|
53
|
+
readonly concrete_table: "roll_down";
|
|
54
|
+
readonly table_per_class: "roll_down";
|
|
55
|
+
};
|
|
56
|
+
readonly merge: {
|
|
57
|
+
readonly flat: "flat";
|
|
58
|
+
readonly flat_with_discriminator: "flat";
|
|
59
|
+
readonly nested: "nested";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export type SupertypeGroupValueSetting = keyof typeof SUPERTYPE_GROUP_VALUES;
|
|
63
|
+
/** The canonical value for a spelling, or undefined when it is not recognized. */
|
|
64
|
+
export declare function canonicalSupertypeGroupValue(setting: SupertypeGroupValueSetting, raw: string): string | undefined;
|
|
65
|
+
/** A group's settings with canonical values. Unrecognized values are left out. */
|
|
66
|
+
export interface SupertypeGroupSettings {
|
|
67
|
+
supertype?: string;
|
|
68
|
+
completeness?: Completeness;
|
|
69
|
+
exclusivity?: Exclusivity;
|
|
70
|
+
strategy?: MaterializationStrategy;
|
|
71
|
+
merge?: MergeOption;
|
|
72
|
+
discriminator?: string;
|
|
73
|
+
note?: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function supertypeGroupSettings(decl: SupertypeGroupDeclaration): SupertypeGroupSettings;
|
|
76
|
+
/** A subtype member's own `strategy` (spec §12.7.4), canonical, or undefined. */
|
|
77
|
+
export declare function subtypeStrategy(member: SupertypeGroupMember): MaterializationStrategy | undefined;
|
|
78
|
+
/** One group with its members resolved to entity ids where they resolve. */
|
|
79
|
+
export interface ResolvedSupertypeGroup {
|
|
80
|
+
declaration: SupertypeGroupDeclaration;
|
|
81
|
+
settings: SupertypeGroupSettings;
|
|
82
|
+
/** Entity id of the supertype, when it resolves. */
|
|
83
|
+
supertype?: string;
|
|
84
|
+
subtypes: Array<{
|
|
85
|
+
member: SupertypeGroupMember;
|
|
86
|
+
/** Entity id of the subtype, when it resolves. */
|
|
87
|
+
entity?: string;
|
|
88
|
+
/** The member's own strategy, canonical, when stated and recognized. */
|
|
89
|
+
strategy?: MaterializationStrategy;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every SupertypeGroup of a document with its members resolved. Pass the
|
|
94
|
+
* flattened document (see `flatten()`) so imported and cloned declarations
|
|
95
|
+
* are visible. Entity ids are container-qualified for entities declared in
|
|
96
|
+
* a Container, bare otherwise.
|
|
97
|
+
*/
|
|
98
|
+
export declare function resolveSupertypeGroups(doc: XDbmlDocument): ResolvedSupertypeGroup[];
|
|
99
|
+
/**
|
|
100
|
+
* For each entity that is a subtype, its chain of supertypes, nearest
|
|
101
|
+
* first: `Employee -> [Person, Party]`. When an entity is listed as a
|
|
102
|
+
* subtype in several groups (an error), the first group wins; a chain that
|
|
103
|
+
* loops stops before repeating an entity.
|
|
104
|
+
*/
|
|
105
|
+
export declare function supertypeChains(doc: XDbmlDocument): Map<string, string[]>;
|
|
106
|
+
/**
|
|
107
|
+
* The rules of spec §12.8. Run by `resolveNames()` on the flattened
|
|
108
|
+
* document; exported for callers that check groups on their own.
|
|
109
|
+
*/
|
|
110
|
+
export declare function checkSupertypeGroups(doc: XDbmlDocument): Diagnostic[];
|