@metaobjectsdev/metadata 1.0.13 → 1.1.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +9 -12
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/attr/meta-attr-filter.d.ts.map +1 -1
- package/dist/core/attr/meta-attr-filter.js +8 -2
- package/dist/core/attr/meta-attr-filter.js.map +1 -1
- package/dist/core/attr/meta-attr.d.ts +14 -0
- package/dist/core/attr/meta-attr.d.ts.map +1 -1
- package/dist/core/attr/meta-attr.js +23 -1
- package/dist/core/attr/meta-attr.js.map +1 -1
- package/dist/core/field/shared-enum.d.ts +21 -0
- package/dist/core/field/shared-enum.d.ts.map +1 -0
- package/dist/core/field/shared-enum.js +49 -0
- package/dist/core/field/shared-enum.js.map +1 -0
- package/dist/core/identity/meta-identity.d.ts +1 -1
- package/dist/core/identity/meta-identity.d.ts.map +1 -1
- package/dist/core/identity/meta-identity.js +8 -13
- package/dist/core/identity/meta-identity.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/object/object-constants.d.ts +14 -1
- package/dist/core/object/object-constants.d.ts.map +1 -1
- package/dist/core/object/object-constants.js +18 -0
- package/dist/core/object/object-constants.js.map +1 -1
- package/dist/core/object/object-definition.embedded.d.ts.map +1 -1
- package/dist/core/object/object-definition.embedded.js +80 -0
- package/dist/core/object/object-definition.embedded.js.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts +1 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.js +15 -44
- package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
- package/dist/core/relationship/find-reference.d.ts.map +1 -1
- package/dist/core/relationship/find-reference.js +4 -2
- package/dist/core/relationship/find-reference.js.map +1 -1
- package/dist/core/relationship/resolve-relationship-reference.d.ts +6 -5
- package/dist/core/relationship/resolve-relationship-reference.d.ts.map +1 -1
- package/dist/core/relationship/resolve-relationship-reference.js +9 -7
- package/dist/core/relationship/resolve-relationship-reference.js.map +1 -1
- package/dist/core/reporting/meta-dimension.d.ts +13 -0
- package/dist/core/reporting/meta-dimension.d.ts.map +1 -0
- package/dist/core/reporting/meta-dimension.js +30 -0
- package/dist/core/reporting/meta-dimension.js.map +1 -0
- package/dist/core/reporting/meta-measure.d.ts +24 -0
- package/dist/core/reporting/meta-measure.d.ts.map +1 -0
- package/dist/core/reporting/meta-measure.js +54 -0
- package/dist/core/reporting/meta-measure.js.map +1 -0
- package/dist/core/reporting/meta-segment.d.ts +6 -0
- package/dist/core/reporting/meta-segment.d.ts.map +1 -0
- package/dist/core/reporting/meta-segment.js +13 -0
- package/dist/core/reporting/meta-segment.js.map +1 -0
- package/dist/core/reporting/report-accessors.d.ts +27 -0
- package/dist/core/reporting/report-accessors.d.ts.map +1 -0
- package/dist/core/reporting/report-accessors.js +56 -0
- package/dist/core/reporting/report-accessors.js.map +1 -0
- package/dist/core/reporting/report-describe.d.ts +59 -0
- package/dist/core/reporting/report-describe.d.ts.map +1 -0
- package/dist/core/reporting/report-describe.js +182 -0
- package/dist/core/reporting/report-describe.js.map +1 -0
- package/dist/core/reporting/report-read-model.d.ts +34 -0
- package/dist/core/reporting/report-read-model.d.ts.map +1 -0
- package/dist/core/reporting/report-read-model.js +165 -0
- package/dist/core/reporting/report-read-model.js.map +1 -0
- package/dist/core/reporting/report-shape.d.ts +103 -0
- package/dist/core/reporting/report-shape.d.ts.map +1 -0
- package/dist/core/reporting/report-shape.js +398 -0
- package/dist/core/reporting/report-shape.js.map +1 -0
- package/dist/core/reporting/reporting-constants.d.ts +65 -0
- package/dist/core/reporting/reporting-constants.d.ts.map +1 -0
- package/dist/core/reporting/reporting-constants.js +84 -0
- package/dist/core/reporting/reporting-constants.js.map +1 -0
- package/dist/core/reporting/reporting-definition.embedded.d.ts +3 -0
- package/dist/core/reporting/reporting-definition.embedded.d.ts.map +1 -0
- package/dist/core/reporting/reporting-definition.embedded.js +185 -0
- package/dist/core/reporting/reporting-definition.embedded.js.map +1 -0
- package/dist/core/requirement/resolve-claim.d.ts +8 -0
- package/dist/core/requirement/resolve-claim.d.ts.map +1 -1
- package/dist/core/requirement/resolve-claim.js +21 -0
- package/dist/core/requirement/resolve-claim.js.map +1 -1
- package/dist/core/yaml-desugar.d.ts.map +1 -1
- package/dist/core/yaml-desugar.js +12 -82
- package/dist/core/yaml-desugar.js.map +1 -1
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +28 -1
- package/dist/core-types.js.map +1 -1
- package/dist/docs-ref.d.ts +19 -0
- package/dist/docs-ref.d.ts.map +1 -0
- package/dist/docs-ref.js +37 -0
- package/dist/docs-ref.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -6
- package/dist/errors.js.map +1 -1
- package/dist/fmt.d.ts.map +1 -1
- package/dist/fmt.js +3 -1
- package/dist/fmt.js.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/loader/declared-duplicate-fields.d.ts +19 -0
- package/dist/loader/declared-duplicate-fields.d.ts.map +1 -0
- package/dist/loader/declared-duplicate-fields.js +97 -0
- package/dist/loader/declared-duplicate-fields.js.map +1 -0
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +18 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/reporting-validation.d.ts +11 -0
- package/dist/loader/reporting-validation.d.ts.map +1 -0
- package/dist/loader/reporting-validation.js +883 -0
- package/dist/loader/reporting-validation.js.map +1 -0
- package/dist/loader/resolve-references.d.ts +8 -0
- package/dist/loader/resolve-references.d.ts.map +1 -0
- package/dist/loader/resolve-references.js +132 -0
- package/dist/loader/resolve-references.js.map +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +14 -9
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/loader/validation-registry.d.ts.map +1 -1
- package/dist/loader/validation-registry.js +33 -10
- package/dist/loader/validation-registry.js.map +1 -1
- package/dist/metamodel-docs/index.js +3 -3
- package/dist/metamodel-docs/index.js.map +1 -1
- package/dist/metamodel-docs/render.js +4 -4
- package/dist/metamodel-docs/render.js.map +1 -1
- package/dist/naming-refs.d.ts +102 -50
- package/dist/naming-refs.d.ts.map +1 -1
- package/dist/naming-refs.js +223 -97
- package/dist/naming-refs.js.map +1 -1
- package/dist/naming.d.ts +42 -7
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +43 -5
- package/dist/naming.js.map +1 -1
- package/dist/object-serializer.d.ts.map +1 -1
- package/dist/object-serializer.js +6 -9
- package/dist/object-serializer.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +18 -48
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/db/db-definition.embedded.js +2 -2
- package/dist/persistence/db/db-definition.embedded.js.map +1 -1
- package/dist/persistence/source/meta-source.d.ts +5 -3
- package/dist/persistence/source/meta-source.d.ts.map +1 -1
- package/dist/persistence/source/meta-source.js +8 -6
- package/dist/persistence/source/meta-source.js.map +1 -1
- package/dist/registry-manifest-exclusions.js +2 -2
- package/dist/registry-manifest-exclusions.js.map +1 -1
- package/dist/registry-manifest.d.ts +1 -1
- package/dist/registry-manifest.js +1 -1
- package/dist/release-docs.d.ts +11 -0
- package/dist/release-docs.d.ts.map +1 -0
- package/dist/release-docs.js +48 -0
- package/dist/release-docs.js.map +1 -0
- package/dist/serializer-json.d.ts +15 -1
- package/dist/serializer-json.d.ts.map +1 -1
- package/dist/serializer-json.js +55 -17
- package/dist/serializer-json.js.map +1 -1
- package/dist/shared/base-types.d.ts +7 -1
- package/dist/shared/base-types.d.ts.map +1 -1
- package/dist/shared/base-types.js +9 -0
- package/dist/shared/base-types.js.map +1 -1
- package/dist/shared/meta-data.d.ts +10 -0
- package/dist/shared/meta-data.d.ts.map +1 -1
- package/dist/shared/meta-data.js +22 -1
- package/dist/shared/meta-data.js.map +1 -1
- package/dist/shared/meta-root.d.ts +13 -1
- package/dist/shared/meta-root.d.ts.map +1 -1
- package/dist/shared/meta-root.js +19 -1
- package/dist/shared/meta-root.js.map +1 -1
- package/dist/super-resolve.d.ts +4 -1
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +44 -65
- package/dist/super-resolve.js.map +1 -1
- package/dist/vocabulary-catalog.js +2 -2
- package/dist/vocabulary-catalog.js.map +1 -1
- package/package.json +6 -1
- package/src/attr-schema-validate.ts +9 -13
- package/src/constants.ts +1 -0
- package/src/core/attr/meta-attr-filter.ts +8 -1
- package/src/core/attr/meta-attr.ts +24 -1
- package/src/core/field/shared-enum.ts +53 -0
- package/src/core/identity/meta-identity.ts +9 -12
- package/src/core/index.ts +5 -0
- package/src/core/object/object-constants.ts +19 -0
- package/src/core/object/object-definition.embedded.ts +80 -0
- package/src/core/relationship/derive-m2m-fields.ts +15 -48
- package/src/core/relationship/find-reference.ts +4 -2
- package/src/core/relationship/resolve-relationship-reference.ts +11 -9
- package/src/core/reporting/meta-dimension.ts +41 -0
- package/src/core/reporting/meta-measure.ts +75 -0
- package/src/core/reporting/meta-segment.ts +14 -0
- package/src/core/reporting/report-accessors.ts +73 -0
- package/src/core/reporting/report-describe.ts +206 -0
- package/src/core/reporting/report-read-model.ts +177 -0
- package/src/core/reporting/report-shape.ts +493 -0
- package/src/core/reporting/reporting-constants.ts +104 -0
- package/src/core/reporting/reporting-definition.embedded.ts +192 -0
- package/src/core/requirement/resolve-claim.ts +20 -0
- package/src/core/yaml-desugar.ts +9 -89
- package/src/core-types.ts +41 -0
- package/src/docs-ref.ts +41 -0
- package/src/errors.ts +17 -6
- package/src/fmt.ts +3 -1
- package/src/index.ts +72 -4
- package/src/loader/declared-duplicate-fields.ts +114 -0
- package/src/loader/meta-data-loader.ts +25 -1
- package/src/loader/reporting-validation.ts +1137 -0
- package/src/loader/resolve-references.ts +144 -0
- package/src/loader/validation-passes.ts +13 -9
- package/src/loader/validation-registry.ts +37 -9
- package/src/metamodel-docs/index.ts +3 -3
- package/src/metamodel-docs/render.ts +4 -4
- package/src/naming-refs.ts +237 -94
- package/src/naming.ts +75 -11
- package/src/object-serializer.ts +7 -9
- package/src/parser-core.ts +19 -56
- package/src/persistence/db/db-definition.embedded.ts +2 -2
- package/src/persistence/source/meta-source.ts +8 -6
- package/src/registry-manifest-exclusions.ts +2 -2
- package/src/registry-manifest.ts +1 -1
- package/src/release-docs.ts +48 -0
- package/src/serializer-json.ts +79 -12
- package/src/shared/base-types.ts +9 -0
- package/src/shared/meta-data.ts +22 -1
- package/src/shared/meta-root.ts +20 -1
- package/src/super-resolve.ts +49 -63
- package/src/vocabulary-catalog.ts +2 -2
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
// FR-044 — cross-node rules for the reporting vocabulary. The rule ids (D1…F2)
|
|
2
|
+
// match the rule table in docs/superpowers/plans/2026-10-03-fr-044-plan-1-reporting-vocabulary.md
|
|
3
|
+
// (R8, R9, M7 and M8: Table B of docs/superpowers/plans/2026-10-09-fr-044-zero-rows-and-measure-defaults.md)
|
|
4
|
+
// and the error fixtures in fixtures/conformance/error-*. Every port implements
|
|
5
|
+
// the same table WITH THE SAME MESSAGE TEXT; the fixtures are the contract.
|
|
6
|
+
//
|
|
7
|
+
// Two design rules hold throughout, so one broken rule yields exactly one error:
|
|
8
|
+
// - No cascades. A member that fails a structural rule is not checked further
|
|
9
|
+
// (a dimension whose @via fails D2 skips D1/D3/D4; a measure that fails one
|
|
10
|
+
// of M0–M4, or whose @default is a fraction, skips M7/M8; a report whose
|
|
11
|
+
// @from fails R1 skips R2/R3/R6/R7, R8/R9 and its @filter; a report whose
|
|
12
|
+
// @spine fails R8 skips R9, and R9
|
|
13
|
+
// skips a dimension whose @via fails D2; an invalid @dimensions/@measures
|
|
14
|
+
// item derives no report field for R6).
|
|
15
|
+
// - Each error's `source` is the offending node (the dimension / measure /
|
|
16
|
+
// segment / report, or for R4/R5 the declared child), so a conformance
|
|
17
|
+
// fixture's jsonPath points at it.
|
|
18
|
+
//
|
|
19
|
+
// Inheritance (ADR-0039): an entity's members are read through children(), so a
|
|
20
|
+
// member declared on an abstract base is validated against every entity that
|
|
21
|
+
// inherits it. Members declared on an entity are validated first (pass 1), then
|
|
22
|
+
// inherited ones (pass 2); an error already reported for the same node with the
|
|
23
|
+
// same message is not repeated, so a broken base member is reported ONCE, and
|
|
24
|
+
// a failure that only an inheritor exposes carries " (inherited by '<entity>')".
|
|
25
|
+
import { ParseError } from "../errors.js";
|
|
26
|
+
import { refTarget, resolveObjectRef } from "../naming-refs.js";
|
|
27
|
+
import { PACKAGE_SEPARATOR, CHILD_REF_SEPARATOR } from "../shared/structural.js";
|
|
28
|
+
import { TYPE_OBJECT, TYPE_FIELD, TYPE_IDENTITY, TYPE_RELATIONSHIP, TYPE_SOURCE, TYPE_DIMENSION, TYPE_MEASURE, TYPE_SEGMENT, } from "../shared/base-types.js";
|
|
29
|
+
import { OBJECT_SUBTYPE_ENTITY, OBJECT_SUBTYPE_REPORT, OBJECT_REPORT_ATTR_FILTER, OBJECT_REPORT_ATTR_SEGMENT, OBJECT_REPORT_ATTR_SPINE, } from "../core/object/object-constants.js";
|
|
30
|
+
import { FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_OBJECT, FIELD_SUBTYPE_MAP, FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIMESTAMP, FIELD_ATTR_OBJECT_REF, } from "../core/field/field-constants.js";
|
|
31
|
+
import { IDENTITY_SUBTYPE_REFERENCE, IDENTITY_REFERENCE_ATTR_REFERENCES, } from "../core/identity/identity-constants.js";
|
|
32
|
+
import { RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_CARDINALITY, CARDINALITY_ONE, } from "../core/relationship/relationship-constants.js";
|
|
33
|
+
import { FILTER_COMPOSE_OR, FILTER_COMPOSE_AND, FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE, opsForField, } from "../core/query/query-constants.js";
|
|
34
|
+
import { ATTR_SUBTYPE_FILTER } from "../core/attr/attr-constants.js";
|
|
35
|
+
import { isMetaObject, isMetaRoot, isReadOnlySource, isWritableSource } from "../shared/node-guards.js";
|
|
36
|
+
import { SOURCE_KIND_VIEW } from "../persistence/source/source-constants.js";
|
|
37
|
+
import { AGG_ALL, AGG_ANY } from "../persistence/origin/origin-constants.js";
|
|
38
|
+
import { FILTER_RELATIVE_NOW, ISO_DURATION_RE, AGG_COUNT, AGG_SUM, AGG_AVG, AGG_MIN, AGG_MAX, GRAIN_HOUR, REPORTING_ATTR_NUMERATOR, REPORTING_ATTR_DENOMINATOR, REPORTING_ATTR_DEFAULT, REPORTING_ATTR_VIA, MEASURE_SUBTYPE_AGGREGATE, } from "../core/reporting/reporting-constants.js";
|
|
39
|
+
import { MetaDimension } from "../core/reporting/meta-dimension.js";
|
|
40
|
+
import { MetaMeasure } from "../core/reporting/meta-measure.js";
|
|
41
|
+
import { MetaSegment } from "../core/reporting/meta-segment.js";
|
|
42
|
+
import { measureResultSubType } from "../core/reporting/report-shape.js";
|
|
43
|
+
import { reportDerivedFieldName, reportDimensionItems, reportFrom, reportMeasureNames, reportSpine, } from "../core/reporting/report-accessors.js";
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Closed sets the rules consult
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/** M4 — the field subtypes `sum`/`avg` accept. */
|
|
48
|
+
const NUMERIC_FIELD_SUBTYPES = [
|
|
49
|
+
FIELD_SUBTYPE_INT,
|
|
50
|
+
FIELD_SUBTYPE_LONG,
|
|
51
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
52
|
+
FIELD_SUBTYPE_FLOAT,
|
|
53
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
54
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
55
|
+
];
|
|
56
|
+
/** M4 — the field subtypes `min`/`max` refuse (no total order). */
|
|
57
|
+
const UNORDERED_FIELD_SUBTYPES = [
|
|
58
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
59
|
+
FIELD_SUBTYPE_OBJECT,
|
|
60
|
+
FIELD_SUBTYPE_MAP,
|
|
61
|
+
];
|
|
62
|
+
/** D3 / F2 — the temporal field subtypes. */
|
|
63
|
+
const TEMPORAL_FIELD_SUBTYPES = [FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIMESTAMP];
|
|
64
|
+
/** F2 — the only ops a relative-date value may sit under. */
|
|
65
|
+
const RELATIVE_DATE_OPS = [FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE];
|
|
66
|
+
const ERR_INVALID_DIMENSION = "ERR_INVALID_DIMENSION";
|
|
67
|
+
const ERR_INVALID_MEASURE = "ERR_INVALID_MEASURE";
|
|
68
|
+
const ERR_INVALID_REPORT = "ERR_INVALID_REPORT";
|
|
69
|
+
const ERR_REPORT_FOREIGN_MEASURE = "ERR_REPORT_FOREIGN_MEASURE";
|
|
70
|
+
const ERR_BAD_ATTR_FILTER = "ERR_BAD_ATTR_FILTER";
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Entry point
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
export function validateReporting(root) {
|
|
75
|
+
const sink = new ErrorSink();
|
|
76
|
+
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
77
|
+
const objects = root.children().filter((c) => c.type === TYPE_OBJECT);
|
|
78
|
+
const entities = objects.filter((o) => o.subType === OBJECT_SUBTYPE_ENTITY);
|
|
79
|
+
// Pass 1: every member against the entity that declares it (an abstract base
|
|
80
|
+
// included — its members must be self-consistent). Pass 2: inherited members
|
|
81
|
+
// against each inheriting entity, so an override that breaks one is caught.
|
|
82
|
+
for (const entity of entities)
|
|
83
|
+
checkEntityMembers(root, entity, true, sink);
|
|
84
|
+
for (const entity of entities)
|
|
85
|
+
checkEntityMembers(root, entity, false, sink);
|
|
86
|
+
for (const report of objects.filter((o) => o.subType === OBJECT_SUBTYPE_REPORT)) {
|
|
87
|
+
checkReport(root, report, sink);
|
|
88
|
+
}
|
|
89
|
+
// F1 on every host that is not a reporting host.
|
|
90
|
+
checkNoRelativeDates(root, sink);
|
|
91
|
+
return sink.errors;
|
|
92
|
+
}
|
|
93
|
+
/** Collects errors, dropping a repeat — the shape an unmodified inherited
|
|
94
|
+
* member's failure takes when it is re-validated under an inheriting entity.
|
|
95
|
+
* A message is `head + suffix + body`. A pass-2 error (suffix
|
|
96
|
+
* " (inherited by '<entity>')") is dropped when pass 1 already reported the
|
|
97
|
+
* same failure without a suffix, or the same inheritor already reported it;
|
|
98
|
+
* a second inheritor's identical failure is still reported, under its own name. */
|
|
99
|
+
class ErrorSink {
|
|
100
|
+
errors = [];
|
|
101
|
+
seen = new Map();
|
|
102
|
+
push(node, code, head, body, suffix = "") {
|
|
103
|
+
// `base` drops a pass-2 copy of a failure pass 1 already reported; a pass-2
|
|
104
|
+
// entry is keyed WITH its suffix, so two inheritors that break the same
|
|
105
|
+
// inherited member the same way are each reported.
|
|
106
|
+
const base = `${code}\u0000${head}${body}`;
|
|
107
|
+
const key = `${base}\u0000${suffix}`;
|
|
108
|
+
let keys = this.seen.get(node);
|
|
109
|
+
if (keys === undefined) {
|
|
110
|
+
keys = new Set();
|
|
111
|
+
this.seen.set(node, keys);
|
|
112
|
+
}
|
|
113
|
+
if (keys.has(base) || keys.has(key))
|
|
114
|
+
return;
|
|
115
|
+
keys.add(suffix === "" ? base : key);
|
|
116
|
+
this.errors.push(new ParseError(`${head}${suffix}${body}`, { code, source: node.source }));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Shared helpers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/** The node's package for ADR-0042 bare-reference resolution. */
|
|
123
|
+
function pkgOf(node) {
|
|
124
|
+
return node.package ?? node.fileDefaultPackage ?? "";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Split a dotted `Owner.child[.child…]` reference at the first `.` after the
|
|
128
|
+
* last `::` (the same rule `_refNamedOwner` applies to extends refs), so an
|
|
129
|
+
* FQN owner (`acme::shop::Purchase.program`) keeps its package. Undefined when
|
|
130
|
+
* there is no owner, no child, or an empty child segment.
|
|
131
|
+
*/
|
|
132
|
+
function splitDotted(ref) {
|
|
133
|
+
const lastSep = ref.lastIndexOf(PACKAGE_SEPARATOR);
|
|
134
|
+
const segStart = lastSep === -1 ? 0 : lastSep + PACKAGE_SEPARATOR.length;
|
|
135
|
+
const dot = ref.indexOf(CHILD_REF_SEPARATOR, segStart);
|
|
136
|
+
if (dot <= segStart)
|
|
137
|
+
return undefined;
|
|
138
|
+
const path = ref.slice(dot + CHILD_REF_SEPARATOR.length).split(CHILD_REF_SEPARATOR);
|
|
139
|
+
if (path.some((s) => s === ""))
|
|
140
|
+
return undefined;
|
|
141
|
+
return { owner: ref.slice(0, dot), path };
|
|
142
|
+
}
|
|
143
|
+
/** True when `candidate` is `entity` or an entity it extends (the super chain). */
|
|
144
|
+
function isSelfOrAncestor(candidate, entity) {
|
|
145
|
+
const visited = new Set();
|
|
146
|
+
for (let n = entity; n !== undefined && !visited.has(n); n = n.superData) {
|
|
147
|
+
if (n === candidate)
|
|
148
|
+
return true;
|
|
149
|
+
visited.add(n);
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function childOfType(obj, type, name) {
|
|
154
|
+
// ADR-0039: resolving — inherited members (via extends) are visible.
|
|
155
|
+
return obj.children().find((c) => c.type === type && c.name === name);
|
|
156
|
+
}
|
|
157
|
+
/** A field's `@objectRef`, read with the resolving accessor (ADR-0039). */
|
|
158
|
+
function objectRefOf(field) {
|
|
159
|
+
return field.attr(FIELD_ATTR_OBJECT_REF);
|
|
160
|
+
}
|
|
161
|
+
function fieldOf(obj, name) {
|
|
162
|
+
return childOfType(obj, TYPE_FIELD, name);
|
|
163
|
+
}
|
|
164
|
+
function isPlainObject(v) {
|
|
165
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
166
|
+
}
|
|
167
|
+
/** An object operand carrying a `now` key — a relative-date value, well-formed
|
|
168
|
+
* (exactly `{ now }`) or not. A malformed one is refused, never read as data. */
|
|
169
|
+
function isRelativeValue(v) {
|
|
170
|
+
return isPlainObject(v) && Object.keys(v).includes(FILTER_RELATIVE_NOW);
|
|
171
|
+
}
|
|
172
|
+
/** True for a well-formed relative value: exactly the one key `now`. */
|
|
173
|
+
function isExactRelativeValue(v) {
|
|
174
|
+
return Object.keys(v).length === 1;
|
|
175
|
+
}
|
|
176
|
+
/** The relative value an op's operand carries: the operand itself, or one inside an array operand. */
|
|
177
|
+
function relativeOperand(v) {
|
|
178
|
+
if (isRelativeValue(v))
|
|
179
|
+
return v;
|
|
180
|
+
if (Array.isArray(v))
|
|
181
|
+
return v.find(isRelativeValue);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
/** Deep search of an operand VALUE: is a relative value (well-formed or not) anywhere inside it? */
|
|
185
|
+
function operandContainsRelativeValue(v) {
|
|
186
|
+
if (isRelativeValue(v))
|
|
187
|
+
return true;
|
|
188
|
+
if (Array.isArray(v))
|
|
189
|
+
return v.some(operandContainsRelativeValue);
|
|
190
|
+
if (isPlainObject(v))
|
|
191
|
+
return Object.values(v).some(operandContainsRelativeValue);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Does a filter contain a relative value in any operand? Walks the filter grammar
|
|
196
|
+
* (`and`/`or` arrays, `{ field: { op: operand } }`) so only operand VALUES are
|
|
197
|
+
* searched: a field key that happens to be named `now` is a field, not a relative date.
|
|
198
|
+
*/
|
|
199
|
+
function filterContainsRelativeValue(filter) {
|
|
200
|
+
if (!isPlainObject(filter))
|
|
201
|
+
return false;
|
|
202
|
+
for (const [key, clause] of Object.entries(filter)) {
|
|
203
|
+
if (key === FILTER_COMPOSE_OR || key === FILTER_COMPOSE_AND) {
|
|
204
|
+
if (Array.isArray(clause) && clause.some(filterContainsRelativeValue))
|
|
205
|
+
return true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (isRelativeValue(clause))
|
|
209
|
+
return true; // un-desugared shorthand
|
|
210
|
+
if (isPlainObject(clause) && Object.values(clause).some(operandContainsRelativeValue))
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function quoteValue(v) {
|
|
216
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
217
|
+
}
|
|
218
|
+
/** `<type>.<subType> '<name>'`, or just `<type>.<subType>` for an unnamed node (e.g. a source or origin). */
|
|
219
|
+
function childLabel(node) {
|
|
220
|
+
const head = `${node.type}.${node.subType}`;
|
|
221
|
+
return node.name !== "" ? `${head} '${node.name}'` : head;
|
|
222
|
+
}
|
|
223
|
+
/** `<type>.<subType> '<FQN>'` for a root-level object, else its childLabel plus ` in <parent label>`. */
|
|
224
|
+
function nodeLabel(node) {
|
|
225
|
+
const parent = node.parent;
|
|
226
|
+
if (parent === undefined || parent.parent === undefined) {
|
|
227
|
+
return `${node.type}.${node.subType} '${node.resolutionKey()}'`;
|
|
228
|
+
}
|
|
229
|
+
return `${childLabel(node)} in ${nodeLabel(parent)}`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The FQN a member message names for `entity`: the DECLARING entity in place of
|
|
233
|
+
* the host, so a failure is worded identically whichever entity reached the
|
|
234
|
+
* member (the suffix names the inheritor) and the ErrorSink repeat test holds.
|
|
235
|
+
*/
|
|
236
|
+
function shown(ctx, entity) {
|
|
237
|
+
return (entity === ctx.host ? ctx.declaring : entity).resolutionKey();
|
|
238
|
+
}
|
|
239
|
+
function checkEntityMembers(root, entity, declaredHere, sink) {
|
|
240
|
+
// ADR-0039: resolving — inherited members are validated against this entity.
|
|
241
|
+
for (const member of entity.children()) {
|
|
242
|
+
if (member.type !== TYPE_DIMENSION && member.type !== TYPE_MEASURE && member.type !== TYPE_SEGMENT)
|
|
243
|
+
continue;
|
|
244
|
+
const declaring = member.parent ?? entity;
|
|
245
|
+
if ((declaring === entity) !== declaredHere)
|
|
246
|
+
continue;
|
|
247
|
+
const ctx = {
|
|
248
|
+
root,
|
|
249
|
+
host: entity,
|
|
250
|
+
declaring,
|
|
251
|
+
label: `${member.type} '${member.name}' on entity '${declaring.resolutionKey()}'`,
|
|
252
|
+
suffix: declaredHere ? "" : ` (inherited by '${entity.resolutionKey()}')`,
|
|
253
|
+
sink,
|
|
254
|
+
};
|
|
255
|
+
if (member instanceof MetaDimension)
|
|
256
|
+
checkDimension(ctx, member);
|
|
257
|
+
else if (member instanceof MetaMeasure)
|
|
258
|
+
checkMeasure(ctx, member);
|
|
259
|
+
else if (member instanceof MetaSegment) {
|
|
260
|
+
const filter = member.filter();
|
|
261
|
+
if (filter !== undefined) {
|
|
262
|
+
checkFilter(filter, entity, declaring.resolutionKey(), ctx.label, member, ctx.suffix, sink);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function checkDimension(ctx, dim) {
|
|
268
|
+
const err = (message) => ctx.sink.push(dim, ERR_INVALID_DIMENSION, ctx.label, `: ${message}`, ctx.suffix);
|
|
269
|
+
// D2 — the @via walk; its terminal is the entity @of must name.
|
|
270
|
+
let ofEntity = ctx.host;
|
|
271
|
+
const via = dim.via();
|
|
272
|
+
if (via !== undefined) {
|
|
273
|
+
const terminal = walkToOneVia(ctx.root, ctx.host, via, err, d2Walk(ctx.declaring));
|
|
274
|
+
if (terminal === undefined)
|
|
275
|
+
return;
|
|
276
|
+
ofEntity = terminal;
|
|
277
|
+
}
|
|
278
|
+
// D1 — @of is Entity.field on the owning entity (or the @via terminal).
|
|
279
|
+
const of = dim.of();
|
|
280
|
+
if (of === undefined)
|
|
281
|
+
return; // missing @of is ERR_MISSING_REQUIRED_ATTR (attr schema pass)
|
|
282
|
+
const parts = splitDotted(of);
|
|
283
|
+
if (parts === undefined || parts.path.length !== 1) {
|
|
284
|
+
err(`@of '${of}' must be Entity.field.`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// D1 — after @via, a bare Owner resolves in the package of the entity @via reaches: the path
|
|
288
|
+
// already fixes the entity, so `Realm.name` after a hop onto `world::Realm` names it. Without
|
|
289
|
+
// @via it resolves in the declaring entity's package.
|
|
290
|
+
const ownerPkg = via === undefined ? pkgOf(ctx.declaring) : pkgOf(ofEntity);
|
|
291
|
+
const named = resolveObjectRef(ctx.root, parts.owner, ownerPkg).node;
|
|
292
|
+
if (!isSelfOrAncestor(named, ofEntity)) {
|
|
293
|
+
if (via === undefined) {
|
|
294
|
+
err(`@of '${of}' must name a field of the owning entity '${ctx.declaring.resolutionKey()}'. ` +
|
|
295
|
+
`Reach another entity's field with @via.`);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
err(`@of '${of}' must name a field of '${shown(ctx, ofEntity)}', the entity @via '${via}' reaches.`);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const fieldName = parts.path[0];
|
|
303
|
+
// With @via the field is looked up on the entity @of NAMES (resolving: its own plus
|
|
304
|
+
// inherited), not on the @via terminal: a base the terminal extends must itself carry the field, or the
|
|
305
|
+
// dimension names a column the base table lacks and `meta migrate` fails on it. Without @via
|
|
306
|
+
// the host IS the checked entity: a dimension inherited from a base is checked against the
|
|
307
|
+
// inheritor's own (possibly overriding) field.
|
|
308
|
+
const lookup = via === undefined ? ofEntity : named;
|
|
309
|
+
const field = fieldOf(lookup, fieldName);
|
|
310
|
+
if (field === undefined) {
|
|
311
|
+
const declaredOnTerminal = lookup !== ofEntity && fieldOf(ofEntity, fieldName) !== undefined;
|
|
312
|
+
err(declaredOnTerminal
|
|
313
|
+
? `@of '${of}' names no field '${fieldName}' on '${shown(ctx, lookup)}': only '${shown(ctx, ofEntity)}', ` +
|
|
314
|
+
`the entity @via reaches, declares it. Name it '${ofEntity.name}.${fieldName}'.`
|
|
315
|
+
: `@of '${of}' names no field '${fieldName}' on '${shown(ctx, lookup)}'.`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// D5 — a dimension groups by a scalar. A field.object (or a field carrying @objectRef) is a JSON
|
|
319
|
+
// document: no port serves a report over one, and a document is not a value to group rows by.
|
|
320
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT || objectRefOf(field) !== undefined) {
|
|
321
|
+
err(`@of '${of}' is a field.${field.subType}${objectRefOf(field) !== undefined ? " with an @objectRef" : ""}, ` +
|
|
322
|
+
`which a report cannot group by. Group by a scalar field of it.`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!dim.isTime())
|
|
326
|
+
return;
|
|
327
|
+
// D3 — a time dimension groups a date or timestamp.
|
|
328
|
+
if (!TEMPORAL_FIELD_SUBTYPES.includes(field.subType)) {
|
|
329
|
+
err(`a time dimension's @of must be a field.date or field.timestamp, but '${of}' is field.${field.subType}.`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// D4 — a date has no hour.
|
|
333
|
+
if (field.subType === FIELD_SUBTYPE_DATE && dim.grains().includes(GRAIN_HOUR)) {
|
|
334
|
+
err(`grain 'hour' is impossible on '${of}', a field.date (a date has no hour). Remove 'hour' from @grains.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** D2 — a dimension's @via, declared on `declaring`. Reproduces D2's messages exactly. */
|
|
338
|
+
function d2Walk(declaring) {
|
|
339
|
+
const declaringKey = declaring.resolutionKey();
|
|
340
|
+
return {
|
|
341
|
+
attr: REPORTING_ATTR_VIA,
|
|
342
|
+
ownerPkg: pkgOf(declaring),
|
|
343
|
+
hostName: declaringKey,
|
|
344
|
+
start: `the owning entity '${declaringKey}'`,
|
|
345
|
+
startShort: "the owning entity",
|
|
346
|
+
toOneReason: "A dimension follows only @cardinality: one relationships and identity.reference hops, so grouping " +
|
|
347
|
+
"can never multiply the measured rows.",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** R8 — a report's @spine, started at @from; Owner resolves in @from's package (it must be
|
|
351
|
+
* @from or an entity @from extends), whatever package the report is in. */
|
|
352
|
+
function spineWalk(from, fromKey) {
|
|
353
|
+
return {
|
|
354
|
+
attr: OBJECT_REPORT_ATTR_SPINE,
|
|
355
|
+
ownerPkg: pkgOf(from),
|
|
356
|
+
hostName: fromKey,
|
|
357
|
+
start: `@from '${fromKey}'`,
|
|
358
|
+
startShort: `@from '${fromKey}'`,
|
|
359
|
+
toOneReason: "A @spine follows only @cardinality: one relationships and identity.reference hops, so each fact row " +
|
|
360
|
+
"joins at most one row of the spine entity and is never counted twice.",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Walk `Owner.hop[.hop...]` from `host`: Owner is `host` or an entity it
|
|
365
|
+
* extends, and every hop is a to-one `relationship.*` or an
|
|
366
|
+
* `identity.reference`. Returns the terminal entity, or undefined after
|
|
367
|
+
* reporting the first failure. D2 and R8 both run it; `walk` says which.
|
|
368
|
+
*/
|
|
369
|
+
function walkToOneVia(root, host, via, err, walk) {
|
|
370
|
+
const named = `@${walk.attr} '${via}'`;
|
|
371
|
+
const nameOf = (entity) => (entity === host ? walk.hostName : entity.resolutionKey());
|
|
372
|
+
const parts = splitDotted(via);
|
|
373
|
+
if (parts === undefined) {
|
|
374
|
+
err(`${named} must be Owner.hop[.hop...], starting at ${walk.startShort}.`);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const owner = resolveObjectRef(root, parts.owner, walk.ownerPkg).node;
|
|
378
|
+
if (!isSelfOrAncestor(owner, host)) {
|
|
379
|
+
err(`${named} must start at ${walk.start}.`);
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
let current = host;
|
|
383
|
+
for (const hopName of parts.path) {
|
|
384
|
+
const hop = childOfType(current, TYPE_RELATIONSHIP, hopName) ??
|
|
385
|
+
current
|
|
386
|
+
.children()
|
|
387
|
+
.find((c) => c.type === TYPE_IDENTITY && c.subType === IDENTITY_SUBTYPE_REFERENCE && c.name === hopName);
|
|
388
|
+
if (hop === undefined) {
|
|
389
|
+
err(`${named} names '${hopName}', which is not a relationship or identity.reference of ` +
|
|
390
|
+
`'${nameOf(current)}'.`);
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
const isReference = hop.type === TYPE_IDENTITY;
|
|
394
|
+
if (!isReference && hop.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_ONE) {
|
|
395
|
+
err(`${named} crosses relationship '${hopName}' on '${nameOf(current)}', which is not to-one. ` +
|
|
396
|
+
walk.toOneReason);
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
// ADR-0059 R3 — a hop target resolves in the package of the node that declares the
|
|
400
|
+
// hop's reference attribute, so an inherited hop means what it meant where it was written.
|
|
401
|
+
const target = refTarget(hop, isReference ? IDENTITY_REFERENCE_ATTR_REFERENCES : RELATIONSHIP_ATTR_OBJECT_REF);
|
|
402
|
+
if (target === undefined) {
|
|
403
|
+
err(`${named} hop '${hopName}' on '${nameOf(current)}' targets no object.`);
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
current = target;
|
|
407
|
+
}
|
|
408
|
+
return current;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The entity a dimension's `@via` reaches, walked from `declaring` with D2's rules,
|
|
412
|
+
* or undefined when the walk fails (D2 reports why). ADR-0059 R3: a dimension's `@of`
|
|
413
|
+
* resolves in this entity's package, so the loader's `resolveReferences` pass reuses
|
|
414
|
+
* the walk rather than restating it.
|
|
415
|
+
*/
|
|
416
|
+
export function dimensionViaTerminal(root, declaring, via) {
|
|
417
|
+
return walkToOneVia(root, declaring, via, () => { }, d2Walk(declaring));
|
|
418
|
+
}
|
|
419
|
+
function checkMeasure(ctx, measure) {
|
|
420
|
+
const err = (message) => ctx.sink.push(measure, ERR_INVALID_MEASURE, ctx.label, `: ${message}`, ctx.suffix);
|
|
421
|
+
// A measure's @default is a whole number: attr.int refuses a fraction (Rule A) and a
|
|
422
|
+
// non-number, so that is the one error. M7-M9 run only when @default is an integral
|
|
423
|
+
// number (Rule B: one mistake, one error).
|
|
424
|
+
const declaredDefault = measure.attr(REPORTING_ATTR_DEFAULT);
|
|
425
|
+
const invalidDefault = declaredDefault !== undefined && !(typeof declaredDefault === "number" && Number.isInteger(declaredDefault));
|
|
426
|
+
if (measure.isRatio()) {
|
|
427
|
+
checkRatioOperands(ctx, measure, err);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (measure.subType !== MEASURE_SUBTYPE_AGGREGATE)
|
|
431
|
+
return;
|
|
432
|
+
const checked = checkAggregateColumns(ctx, measure, err);
|
|
433
|
+
const clean = checked !== undefined;
|
|
434
|
+
const ofField = checked?.field;
|
|
435
|
+
// M7 / M8 — where a @default can apply. Only when M0–M4 passed and the @default is
|
|
436
|
+
// an integral number: one mistake, one error (the attribute type check reports a bad value).
|
|
437
|
+
if (clean && !invalidDefault && declaredDefault !== undefined) {
|
|
438
|
+
const agg = measure.agg();
|
|
439
|
+
if (agg === AGG_COUNT) {
|
|
440
|
+
err(`@default cannot apply to @agg: count. A count is never null (it is 0 when nothing matches); ` +
|
|
441
|
+
`remove @default.`);
|
|
442
|
+
}
|
|
443
|
+
else if ((agg === AGG_ANY || agg === AGG_ALL) &&
|
|
444
|
+
typeof declaredDefault === "number" &&
|
|
445
|
+
declaredDefault !== 0 &&
|
|
446
|
+
declaredDefault !== 1) {
|
|
447
|
+
// M9 — a boolean measure's @default is 0 (false) or 1 (true).
|
|
448
|
+
err(`@default '${declaredDefault}' on @agg '${agg}' must be 0 (false) or 1 (true): the measure is a boolean.`);
|
|
449
|
+
}
|
|
450
|
+
else if ((agg === AGG_MIN || agg === AGG_MAX) &&
|
|
451
|
+
ofField !== undefined &&
|
|
452
|
+
!NUMERIC_FIELD_SUBTYPES.includes(ofField.subType)) {
|
|
453
|
+
err(`@default is a number, but '${measure.ofColumns()[0]}', the @of of @agg '${agg}', is a ` +
|
|
454
|
+
`field.${ofField.subType}. A default is supported on numeric measures only.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// M5 — @segment names a segment of the owning entity.
|
|
458
|
+
const segment = measure.segmentName();
|
|
459
|
+
if (segment !== undefined && childOfType(ctx.host, TYPE_SEGMENT, segment) === undefined) {
|
|
460
|
+
err(`@segment '${segment}' names no segment of '${ctx.declaring.resolutionKey()}'.`);
|
|
461
|
+
}
|
|
462
|
+
// S1 / F2 — the measure's own row scope.
|
|
463
|
+
const filter = measure.filter();
|
|
464
|
+
if (filter !== undefined) {
|
|
465
|
+
checkFilter(filter, ctx.host, ctx.declaring.resolutionKey(), ctx.label, measure, ctx.suffix, ctx.sink);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* M0–M4, in order; the first failure stops the chain (no M2+M3 double report).
|
|
470
|
+
* Returns undefined when one of them fired; otherwise `field` is the single
|
|
471
|
+
* resolved @of field (undefined for a tuple), for M7/M8.
|
|
472
|
+
*/
|
|
473
|
+
function checkAggregateColumns(ctx, measure, err) {
|
|
474
|
+
const agg = measure.agg();
|
|
475
|
+
const columns = measure.ofColumns();
|
|
476
|
+
// M0 — @of names the column aggregated. Only any / all may omit it: they then quantify the
|
|
477
|
+
// measure's row condition (@filter AND @segment) over the group, "some row (every row)
|
|
478
|
+
// passes", so they need a condition to quantify.
|
|
479
|
+
if (columns.length === 0 && agg !== undefined) {
|
|
480
|
+
if (agg !== AGG_ANY && agg !== AGG_ALL) {
|
|
481
|
+
err(`@agg '${agg}' needs @of, the column it aggregates. Only @agg any / all may omit @of, to ask ` +
|
|
482
|
+
`whether some row (any) or every row (all) passes the measure's @filter or @segment.`);
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
if (measure.filter() === undefined && measure.segmentName() === undefined) {
|
|
486
|
+
err(`@agg '${agg}' without @of quantifies the measure's row condition, but it declares neither ` +
|
|
487
|
+
`@filter nor @segment. Name a field.boolean in @of, or add the condition.`);
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
return { field: undefined };
|
|
491
|
+
}
|
|
492
|
+
// M1 — every @of item is a field of the owning entity.
|
|
493
|
+
const fields = [];
|
|
494
|
+
for (const item of columns) {
|
|
495
|
+
const parts = splitDotted(item);
|
|
496
|
+
if (parts === undefined || parts.path.length !== 1) {
|
|
497
|
+
err(`@of '${item}' must be Entity.field.`);
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
const named = resolveObjectRef(ctx.root, parts.owner, pkgOf(ctx.declaring)).node;
|
|
501
|
+
if (!isSelfOrAncestor(named, ctx.host)) {
|
|
502
|
+
err(`@of '${item}' must name a field of the owning entity '${ctx.declaring.resolutionKey()}'. ` +
|
|
503
|
+
`A measure aggregates its own entity's rows; declare it on the entity that owns the column.`);
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
const field = fieldOf(ctx.host, parts.path[0]);
|
|
507
|
+
if (field === undefined) {
|
|
508
|
+
err(`@of '${item}' names no field '${parts.path[0]}' on '${ctx.declaring.resolutionKey()}'.`);
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
// M10 — a measure aggregates scalars. A field.object (or a field carrying @objectRef) is a JSON
|
|
512
|
+
// document, which no port aggregates in a report.
|
|
513
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT || objectRefOf(field) !== undefined) {
|
|
514
|
+
err(`@of '${item}' is a field.${field.subType}${objectRefOf(field) !== undefined ? " with an @objectRef" : ""}, ` +
|
|
515
|
+
`which a report cannot aggregate. Aggregate a scalar field of it.`);
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
fields.push(field);
|
|
519
|
+
}
|
|
520
|
+
// M2 — a tuple is a distinct count only.
|
|
521
|
+
if (columns.length > 1 && (agg !== AGG_COUNT || !measure.distinct())) {
|
|
522
|
+
err(`@of lists ${columns.length} columns; a tuple is legal only with @agg: count and @distinct: true ` +
|
|
523
|
+
`(a distinct count of the tuple).`);
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
// M3 — @distinct is a count modifier.
|
|
527
|
+
if (measure.distinct() && agg !== undefined && agg !== AGG_COUNT) {
|
|
528
|
+
err(`@distinct: true requires @agg: count, not '${agg}'.`);
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
// M4 — the aggregate must be meaningful for the column's type.
|
|
532
|
+
const field = fields.length === 1 ? fields[0] : undefined;
|
|
533
|
+
if (field === undefined || agg === undefined)
|
|
534
|
+
return { field };
|
|
535
|
+
const item = columns[0];
|
|
536
|
+
if ((agg === AGG_SUM || agg === AGG_AVG) && !NUMERIC_FIELD_SUBTYPES.includes(field.subType)) {
|
|
537
|
+
err(`@agg '${agg}' needs a numeric field (field.int, long, double, float, decimal or currency), ` +
|
|
538
|
+
`but '${item}' is field.${field.subType}.`);
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
if ((agg === AGG_MIN || agg === AGG_MAX) && UNORDERED_FIELD_SUBTYPES.includes(field.subType)) {
|
|
542
|
+
err(`@agg '${agg}' cannot order '${item}', a field.${field.subType}.`);
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
if ((agg === AGG_ANY || agg === AGG_ALL) && field.subType !== FIELD_SUBTYPE_BOOLEAN) {
|
|
546
|
+
err(`@agg '${agg}' needs a field.boolean, but '${item}' is field.${field.subType}.`);
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
return { field };
|
|
550
|
+
}
|
|
551
|
+
/** M6 — each operand names a measure.aggregate of the same entity whose result is a number. */
|
|
552
|
+
function checkRatioOperands(ctx, ratio, err) {
|
|
553
|
+
const operands = [
|
|
554
|
+
[REPORTING_ATTR_NUMERATOR, ratio.numerator()],
|
|
555
|
+
[REPORTING_ATTR_DENOMINATOR, ratio.denominator()],
|
|
556
|
+
];
|
|
557
|
+
for (const [attr, ref] of operands) {
|
|
558
|
+
if (ref === undefined)
|
|
559
|
+
continue; // missing operand is ERR_MISSING_REQUIRED_ATTR
|
|
560
|
+
const target = childOfType(ctx.host, TYPE_MEASURE, ref);
|
|
561
|
+
if (target === undefined) {
|
|
562
|
+
err(`@${attr} '${ref}' names no measure of '${ctx.declaring.resolutionKey()}'.`);
|
|
563
|
+
}
|
|
564
|
+
else if (target.subType !== MEASURE_SUBTYPE_AGGREGATE) {
|
|
565
|
+
err(`@${attr} '${ref}' is a measure.${target.subType}; a ratio's operands must be measure.aggregate ` +
|
|
566
|
+
`(a ratio of ratios is not supported).`);
|
|
567
|
+
}
|
|
568
|
+
else if (target instanceof MetaMeasure && isMetaObject(ctx.host) && isMetaRoot(ctx.root)) {
|
|
569
|
+
const result = measureResultSubType(target, ctx.host, ctx.root);
|
|
570
|
+
if (result !== undefined && !NUMERIC_FIELD_SUBTYPES.includes(result)) {
|
|
571
|
+
err(`@${attr} '${ref}' is a ${result} measure; a ratio divides numbers, so its operands must be numeric ` +
|
|
572
|
+
`measures (a count, a sum, an avg, or a min or max of a numeric field).`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// S1 / F2 — a reporting-host @filter over its entity
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
/**
|
|
581
|
+
* Validate a canonical (post-desugar) attr.filter against `entity`'s fields:
|
|
582
|
+
* every key names a field (S1), every op is legal for that field (S1), and a
|
|
583
|
+
* relative-date operand sits on a date/timestamp, under a range op, with a
|
|
584
|
+
* valid ISO-8601 duration (F2). One error per offending clause op.
|
|
585
|
+
*/
|
|
586
|
+
function checkFilter(filter, entity, entityKey, hostLabel, host, suffix, sink) {
|
|
587
|
+
const err = (message) => sink.push(host, ERR_BAD_ATTR_FILTER, hostLabel, `: ${message}`, suffix);
|
|
588
|
+
for (const [key, clause] of Object.entries(filter)) {
|
|
589
|
+
if (key === FILTER_COMPOSE_OR || key === FILTER_COMPOSE_AND) {
|
|
590
|
+
if (!Array.isArray(clause)) {
|
|
591
|
+
err(`@filter '${key}' must be an array of sub-clauses.`);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
for (const sub of clause) {
|
|
595
|
+
if (isPlainObject(sub))
|
|
596
|
+
checkFilter(sub, entity, entityKey, hostLabel, host, suffix, sink);
|
|
597
|
+
else
|
|
598
|
+
err(`@filter '${key}' contains a non-object sub-clause.`);
|
|
599
|
+
}
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const field = fieldOf(entity, key);
|
|
603
|
+
if (field === undefined) {
|
|
604
|
+
err(`@filter names '${key}', which is not a field of '${entityKey}'.`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (!isPlainObject(clause) || Object.keys(clause).length === 0) {
|
|
608
|
+
err(`@filter on '${key}' must be an { op: value } object.`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const allowed = opsForField(field);
|
|
612
|
+
for (const [op, operand] of Object.entries(clause)) {
|
|
613
|
+
if (!allowed.includes(op)) {
|
|
614
|
+
err(`@filter on '${key}' uses op '${op}', which is not allowed for field.${field.subType}. ` +
|
|
615
|
+
`Allowed ops: ${allowed.join(", ") || "(none)"}.`);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const relative = relativeOperand(operand);
|
|
619
|
+
if (relative === undefined)
|
|
620
|
+
continue;
|
|
621
|
+
if (!isExactRelativeValue(relative)) {
|
|
622
|
+
err(`@filter on '${key}' has a malformed relative date ${JSON.stringify(relative)}; a relative date is ` +
|
|
623
|
+
`exactly { now: "<ISO-8601 duration>" } with no other keys.`);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (!TEMPORAL_FIELD_SUBTYPES.includes(field.subType)) {
|
|
627
|
+
err(`@filter on '${key}' uses a relative date ({ now: ... }), but '${key}' is field.${field.subType}; ` +
|
|
628
|
+
`relative dates apply only to field.date and field.timestamp.`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (!RELATIVE_DATE_OPS.includes(op)) {
|
|
632
|
+
err(`@filter on '${key}' puts a relative date under op '${op}'; relative dates are legal only under ` +
|
|
633
|
+
`gt, gte, lt and lte.`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const duration = relative[FILTER_RELATIVE_NOW];
|
|
637
|
+
if (typeof duration !== "string" || !ISO_DURATION_RE.test(duration)) {
|
|
638
|
+
err(`@filter on '${key}' has relative date '${quoteValue(duration)}', which is not an ISO-8601 ` +
|
|
639
|
+
`duration (e.g. '-P7D', '-PT12H').`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
// R1–R7 — object.report
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
function checkReport(root, report, sink) {
|
|
648
|
+
const label = `report '${report.resolutionKey()}'`;
|
|
649
|
+
const err = (message, node = report, code = ERR_INVALID_REPORT) => sink.push(node, code, label, message);
|
|
650
|
+
// R4 — a report's fields and identity are derived, never declared.
|
|
651
|
+
// ADR-0039: own — the rule is about what the author declared on THIS report.
|
|
652
|
+
for (const child of report.ownChildren()) {
|
|
653
|
+
if (child.type === TYPE_FIELD || child.type === TYPE_IDENTITY) {
|
|
654
|
+
err(` declares ${childLabel(child)}; a report's fields and identity are derived ` +
|
|
655
|
+
`from @dimensions and @measures, never declared.`, child);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// R5 — a report is read-only, so any source it has is read-only.
|
|
659
|
+
// ADR-0039: resolving — an inherited source binds the report just the same.
|
|
660
|
+
for (const source of report.children().filter((c) => c.type === TYPE_SOURCE)) {
|
|
661
|
+
if (isWritableSource(source)) {
|
|
662
|
+
err(`: ${childLabel(source)} is writable; a report is read-only, so its source must ` +
|
|
663
|
+
`declare a read-only @kind (view, materializedView, storedProc or tableFunction).`, source);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// R10 — a report is read through a view. The other read-only kinds are not read by a plain
|
|
667
|
+
// SELECT (a stored procedure and a table function are called, not selected from), and no port
|
|
668
|
+
// lowers or serves them. A view or materialized view the adopter creates is declared
|
|
669
|
+
// `@kind: view` with `@unmanaged: true`; a SELECT reads either one.
|
|
670
|
+
// ADR-0039: resolving, as R5 reads it.
|
|
671
|
+
for (const source of report.children().filter(isReadOnlySource)) {
|
|
672
|
+
if (source.effectiveKind !== SOURCE_KIND_VIEW) {
|
|
673
|
+
err(`: ${childLabel(source)} has @kind '${source.effectiveKind}'; a report is read through a view, so its ` +
|
|
674
|
+
`source must be @kind: view. For a view or materialized view you create yourself, declare ` +
|
|
675
|
+
`@kind: view with @unmanaged: true.`, source);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// R11 — a report cannot be abstract: its fields are derived from its own @dimensions and
|
|
679
|
+
// @measures, no port reads or serves an abstract report, and report inheritance is not part of
|
|
680
|
+
// the vocabulary.
|
|
681
|
+
if (report.isAbstract) {
|
|
682
|
+
err(`: is abstract; a report cannot be abstract, because its fields are derived from its own @dimensions ` +
|
|
683
|
+
`and @measures and no port reads or serves an abstract report. Remove abstract: true.`);
|
|
684
|
+
}
|
|
685
|
+
// R1 — @from resolves to an object.entity. Without it, R2/R3/R6/R7, R8/R9 and
|
|
686
|
+
// the @filter have nothing to resolve against, so they are skipped.
|
|
687
|
+
const fromRef = reportFrom(report);
|
|
688
|
+
if (fromRef === undefined)
|
|
689
|
+
return; // missing @from is ERR_MISSING_REQUIRED_ATTR
|
|
690
|
+
const from = resolveObjectRef(root, fromRef, pkgOf(report)).node;
|
|
691
|
+
if (from === undefined) {
|
|
692
|
+
err(`: @from '${fromRef}' does not resolve to an object.`);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (from.type !== TYPE_OBJECT || from.subType !== OBJECT_SUBTYPE_ENTITY) {
|
|
696
|
+
err(`: @from '${fromRef}' is an ${from.type}.${from.subType}; a report aggregates the rows of an object.entity.`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const fromKey = from.resolutionKey();
|
|
700
|
+
// R6 — derived field name -> the item that derived it ("dimension item 'x'" / "measure 'y'").
|
|
701
|
+
const derived = new Map();
|
|
702
|
+
const claim = (fieldName, what) => {
|
|
703
|
+
const prior = derived.get(fieldName);
|
|
704
|
+
if (prior === what) {
|
|
705
|
+
// The same measure listed twice: name the repeat, not a "collision" with itself.
|
|
706
|
+
err(`: @measures lists '${fieldName}' more than once.`);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (prior !== undefined) {
|
|
710
|
+
err(`: ${prior} and ${what} both derive report field '${fieldName}'. Report field names must be unique; ` +
|
|
711
|
+
`rename the measure or drop one item.`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
derived.set(fieldName, what);
|
|
715
|
+
};
|
|
716
|
+
// R2 — each @dimensions item names a dimension of @from, with a grain exactly when it is a time dimension.
|
|
717
|
+
const seenItems = new Set();
|
|
718
|
+
for (const item of reportDimensionItems(report)) {
|
|
719
|
+
const raw = item.grain === undefined ? item.name : `${item.name}:${item.grain}`;
|
|
720
|
+
if (seenItems.has(raw)) {
|
|
721
|
+
err(`: @dimensions lists '${raw}' more than once.`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
seenItems.add(raw);
|
|
725
|
+
const dim = childOfType(from, TYPE_DIMENSION, item.name);
|
|
726
|
+
if (!(dim instanceof MetaDimension)) {
|
|
727
|
+
err(`: @dimensions item '${raw}' names no dimension of @from '${fromKey}'.`);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (dim.isTime()) {
|
|
731
|
+
const grains = dim.grains();
|
|
732
|
+
if (item.grain === undefined) {
|
|
733
|
+
err(`: @dimensions item '${raw}' names time dimension '${item.name}' without a grain; write ` +
|
|
734
|
+
`'${item.name}:<grain>' with a grain from its @grains (${grains.join(", ")}).`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (!grains.includes(item.grain)) {
|
|
738
|
+
err(`: @dimensions item '${raw}' uses grain '${item.grain}', which time dimension '${item.name}' does not ` +
|
|
739
|
+
`declare. Its @grains: ${grains.join(", ")}.`);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else if (item.grain !== undefined) {
|
|
744
|
+
err(`: @dimensions item '${raw}' gives a grain to attribute dimension '${item.name}'; only a time ` +
|
|
745
|
+
`dimension takes a grain.`);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
claim(reportDerivedFieldName(item), `dimension item '${raw}'`);
|
|
749
|
+
}
|
|
750
|
+
// R3 — each @measures item names a measure of @from.
|
|
751
|
+
for (const item of reportMeasureNames(report)) {
|
|
752
|
+
const measureName = checkReportMeasure(root, report, from, item, label, sink);
|
|
753
|
+
if (measureName !== undefined)
|
|
754
|
+
claim(measureName, `measure '${measureName}'`);
|
|
755
|
+
}
|
|
756
|
+
// R7 — @segment names a segment of @from.
|
|
757
|
+
const segment = report.attr(OBJECT_REPORT_ATTR_SEGMENT);
|
|
758
|
+
if (typeof segment === "string" && childOfType(from, TYPE_SEGMENT, segment) === undefined) {
|
|
759
|
+
err(`: @segment '${segment}' names no segment of @from '${fromKey}'.`);
|
|
760
|
+
}
|
|
761
|
+
// R8 — @spine is a to-one path from @from: rule D2's walk, started at @from.
|
|
762
|
+
const spine = reportSpine(report);
|
|
763
|
+
if (spine !== undefined) {
|
|
764
|
+
const terminal = walkToOneVia(root, from, spine, (message) => err(`: ${message}`), spineWalk(from, fromKey));
|
|
765
|
+
// R9 — every listed dimension is reached through the spine. Skipped when R8 failed.
|
|
766
|
+
if (terminal !== undefined) {
|
|
767
|
+
const spineHops = splitDotted(spine)?.path ?? [];
|
|
768
|
+
const items = reportDimensionItems(report);
|
|
769
|
+
if (items.length === 0) {
|
|
770
|
+
err(`: @spine '${spine}' needs at least one dimension. The report's rows are the dimension tuples of ` +
|
|
771
|
+
`'${terminal.resolutionKey()}'; with no dimension it would be one totals row.`);
|
|
772
|
+
}
|
|
773
|
+
// One verdict per dimension, however many grains list it.
|
|
774
|
+
const checkedDimensions = new Set();
|
|
775
|
+
for (const item of items) {
|
|
776
|
+
if (checkedDimensions.has(item.name))
|
|
777
|
+
continue;
|
|
778
|
+
checkedDimensions.add(item.name);
|
|
779
|
+
const dim = childOfType(from, TYPE_DIMENSION, item.name);
|
|
780
|
+
if (!(dim instanceof MetaDimension))
|
|
781
|
+
continue; // R2 already reported it
|
|
782
|
+
const via = dim.via();
|
|
783
|
+
if (via === undefined) {
|
|
784
|
+
err(`: dimension '${item.name}' is read from @from '${fromKey}', so it has no value in a row that has ` +
|
|
785
|
+
`no facts. With @spine '${spine}' every dimension must be reached through it: declare the ` +
|
|
786
|
+
`dimension over a field of '${terminal.resolutionKey()}' (or an entity to-one from it) with an ` +
|
|
787
|
+
`@via that begins '${spine}'.`);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
// A @via that does not walk is D2's error, on the dimension; R9 does not report it again.
|
|
791
|
+
const discard = () => undefined;
|
|
792
|
+
if (walkToOneVia(root, from, via, discard, d2Walk(dim.parent ?? from)) === undefined)
|
|
793
|
+
continue;
|
|
794
|
+
// Hop names are compared as written; the owner segment is not compared.
|
|
795
|
+
const hops = splitDotted(via)?.path ?? [];
|
|
796
|
+
if (spineHops.every((h, i) => hops[i] === h))
|
|
797
|
+
continue;
|
|
798
|
+
err(`: dimension '${item.name}' is reached by @via '${via}', which does not begin with the hops of ` +
|
|
799
|
+
`@spine '${spine}'. Hop names are compared as written: if both name the same join, write the same ` +
|
|
800
|
+
`hops; otherwise the dimension is not reached through the spine.`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// S1 / F2 — the report's row scope over @from.
|
|
805
|
+
const filter = report.attr(OBJECT_REPORT_ATTR_FILTER);
|
|
806
|
+
if (isPlainObject(filter))
|
|
807
|
+
checkFilter(filter, from, fromKey, label, report, "", sink);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* R3 for one `@measures` item (bare `name` or dotted `Entity.name`). Returns
|
|
811
|
+
* the measure's name when it is a measure of @from (for R6), else reports
|
|
812
|
+
* ERR_REPORT_FOREIGN_MEASURE (it is another entity's measure) or
|
|
813
|
+
* ERR_INVALID_REPORT (it is nobody's) and returns undefined.
|
|
814
|
+
*/
|
|
815
|
+
function checkReportMeasure(root, report, from, item, label, sink) {
|
|
816
|
+
const fromKey = from.resolutionKey();
|
|
817
|
+
let owner;
|
|
818
|
+
let name;
|
|
819
|
+
const parts = splitDotted(item);
|
|
820
|
+
if (parts !== undefined && parts.path.length === 1) {
|
|
821
|
+
owner = resolveObjectRef(root, parts.owner, pkgOf(report)).node;
|
|
822
|
+
name = parts.path[0];
|
|
823
|
+
if (owner !== undefined && isSelfOrAncestor(owner, from) && childOfType(from, TYPE_MEASURE, name))
|
|
824
|
+
return name;
|
|
825
|
+
if (owner !== undefined && childOfType(owner, TYPE_MEASURE, name) === undefined)
|
|
826
|
+
owner = undefined;
|
|
827
|
+
}
|
|
828
|
+
else if (!item.includes(CHILD_REF_SEPARATOR)) {
|
|
829
|
+
name = item;
|
|
830
|
+
if (childOfType(from, TYPE_MEASURE, name) !== undefined)
|
|
831
|
+
return name;
|
|
832
|
+
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
833
|
+
owner = root
|
|
834
|
+
.children()
|
|
835
|
+
.find((o) => o.type === TYPE_OBJECT &&
|
|
836
|
+
o.subType === OBJECT_SUBTYPE_ENTITY &&
|
|
837
|
+
childOfType(o, TYPE_MEASURE, name) !== undefined);
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
owner = undefined;
|
|
841
|
+
}
|
|
842
|
+
if (owner !== undefined) {
|
|
843
|
+
const ownerKey = owner.resolutionKey();
|
|
844
|
+
sink.push(report, ERR_REPORT_FOREIGN_MEASURE, label, ` lists measure '${item.slice(item.lastIndexOf(CHILD_REF_SEPARATOR) + 1)}', which belongs to ` +
|
|
845
|
+
`'${ownerKey}', not @from '${fromKey}'. All measures of a report come from @from; make a second ` +
|
|
846
|
+
`report over '${ownerKey}'.`);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
sink.push(report, ERR_INVALID_REPORT, label, `: @measures item '${item}' names no measure of @from '${fromKey}' or of any other entity.`);
|
|
850
|
+
}
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
// ---------------------------------------------------------------------------
|
|
854
|
+
// F1 — relative-date values only on reporting hosts
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
/** True for the hosts whose `@filter` may carry a relative-date value. */
|
|
857
|
+
function isReportingFilterHost(node) {
|
|
858
|
+
return (node.type === TYPE_SEGMENT ||
|
|
859
|
+
(node.type === TYPE_MEASURE && node.subType === MEASURE_SUBTYPE_AGGREGATE) ||
|
|
860
|
+
(node.type === TYPE_OBJECT && node.subType === OBJECT_SUBTYPE_REPORT));
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* F1 — walk the whole tree and refuse a `{ now: ... }` value in any attr.filter
|
|
864
|
+
* outside a reporting host (a projection @filter, a dataGrid preset, an
|
|
865
|
+
* origin.aggregate/first @filter): those hosts have no lowering for it.
|
|
866
|
+
*/
|
|
867
|
+
function checkNoRelativeDates(node, sink) {
|
|
868
|
+
if (!isReportingFilterHost(node)) {
|
|
869
|
+
// ADR-0039: own — only locally declared filters are lowered, and the walk
|
|
870
|
+
// visits every declared node exactly once (an inherited filter is checked
|
|
871
|
+
// where it is declared; origin.* never inherits, ADR-0029).
|
|
872
|
+
for (const attr of node.ownMetaAttrs()) {
|
|
873
|
+
if (attr.subType === ATTR_SUBTYPE_FILTER && filterContainsRelativeValue(attr.value)) {
|
|
874
|
+
sink.push(node, ERR_BAD_ATTR_FILTER, nodeLabel(node), `: @${attr.name} uses a relative date ({ now: ... }), which is legal only in the ` +
|
|
875
|
+
`@filter of a segment, measure.aggregate or object.report.`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// ADR-0039: own — a tree walk; each declared node is visited once, at its declaration.
|
|
880
|
+
for (const child of node.ownChildren())
|
|
881
|
+
checkNoRelativeDates(child, sink);
|
|
882
|
+
}
|
|
883
|
+
//# sourceMappingURL=reporting-validation.js.map
|