@metaobjectsdev/metadata 1.0.12 → 1.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/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 +24 -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 +65 -7
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +98 -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 -2
- package/dist/persistence/source/meta-source.d.ts.map +1 -1
- package/dist/persistence/source/meta-source.js +10 -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 +24 -6
- package/src/fmt.ts +3 -1
- package/src/index.ts +73 -5
- 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 +131 -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 +10 -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
|
@@ -45,7 +45,7 @@ import type { MetaData } from "./shared/meta-data.js";
|
|
|
45
45
|
import { ParseError } from "./errors.js";
|
|
46
46
|
import type { AttrSchema, TypeRegistry } from "./registry.js";
|
|
47
47
|
import { childRuleMatches } from "./registry.js";
|
|
48
|
-
import { TYPE_FIELD
|
|
48
|
+
import { TYPE_FIELD } from "./shared/base-types.js";
|
|
49
49
|
import { ATTR_SUBTYPE_PROPERTIES } from "./core/attr/attr-constants.js";
|
|
50
50
|
// #337: turns "unknown X" into "retired in V — here is the migration". Changes no load
|
|
51
51
|
// outcome; retired vocabulary still fails with the same code.
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
FIELD_ATTR_INT_VALUE_MAP,
|
|
59
59
|
ENUM_MEMBER_PATTERN,
|
|
60
60
|
} from "./core/field/field-constants.js";
|
|
61
|
+
import { isSharedEnumDeclaration } from "./core/field/shared-enum.js";
|
|
61
62
|
import {
|
|
62
63
|
FIELD_ATTR_DB_COLUMN_TYPE,
|
|
63
64
|
DB_COLUMN_TYPE_VALUES,
|
|
@@ -337,18 +338,13 @@ function validateNode(
|
|
|
337
338
|
// only own @values need checking here (mirrors the own-attrs-only policy of
|
|
338
339
|
// Checks 2+3 above — inherited attrs were validated on the declaring node).
|
|
339
340
|
if (node.type === TYPE_FIELD && node.subType === FIELD_SUBTYPE_ENUM) {
|
|
340
|
-
// #246: the shared-enum super, if any
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
const sharedSuper =
|
|
347
|
-
node.superData !== undefined &&
|
|
348
|
-
node.superData.isAbstract &&
|
|
349
|
-
node.superData.parent?.type === TYPE_METADATA
|
|
350
|
-
? node.superData
|
|
351
|
-
: undefined;
|
|
341
|
+
// #246: the shared-enum super, if any. FR-019 materializes a shared declaration
|
|
342
|
+
// ONCE per port as a single named type, so anything a field directly under it
|
|
343
|
+
// re-declares that is part of the shared TYPE's contract is a conflict. The
|
|
344
|
+
// predicate is the one codegen's resolver walks with (core/field/shared-enum.ts).
|
|
345
|
+
// Only the IMMEDIATE super is checked: a redeclaration further down the chain is
|
|
346
|
+
// legal, and the resolver answers it with a per-object inline type.
|
|
347
|
+
const sharedSuper = isSharedEnumDeclaration(node.superData) ? node.superData : undefined;
|
|
352
348
|
|
|
353
349
|
// ADR-0039: own — validates the @values DECLARED on this node; a concrete enum
|
|
354
350
|
// extending an abstract one inherits already-validated @values (own-attrs-only).
|
package/src/constants.ts
CHANGED
|
@@ -45,6 +45,7 @@ export * from "./core/index/index-constants.js";
|
|
|
45
45
|
export * from "./core/object/object-constants.js";
|
|
46
46
|
export * from "./core/query/query-constants.js";
|
|
47
47
|
export * from "./core/relationship/relationship-constants.js";
|
|
48
|
+
export * from "./core/reporting/reporting-constants.js";
|
|
48
49
|
export * from "./core/validator/validator-constants.js";
|
|
49
50
|
export * from "./persistence/db/db-constants.js";
|
|
50
51
|
export * from "./persistence/origin/origin-constants.js";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
FILTER_COMPOSE_OR,
|
|
15
15
|
FILTER_COMPOSE_AND,
|
|
16
16
|
} from "../query/query-constants.js";
|
|
17
|
+
import { FILTER_RELATIVE_NOW } from "../reporting/reporting-constants.js";
|
|
17
18
|
import { registerAttrClass } from "../../attr-class-map.js";
|
|
18
19
|
|
|
19
20
|
export class FilterAttr extends MetaAttr {
|
|
@@ -60,7 +61,13 @@ function desugarFilterObject(filter: AttrObject): AttrObject {
|
|
|
60
61
|
function desugarClause(raw: AttrJson): AttrObject {
|
|
61
62
|
if (raw === null) return { [FILTER_OP_IS_NULL]: true };
|
|
62
63
|
if (Array.isArray(raw)) return { [FILTER_OP_IN]: raw };
|
|
63
|
-
|
|
64
|
+
// FR-044 F2: an object carrying a `now` key is a relative-date VALUE, never an
|
|
65
|
+
// op map (`now` is not an operator). Shorthand `{ f: { now: "-P7D" } }` therefore
|
|
66
|
+
// means `eq` like every other shorthand value, and survives as an opaque operand;
|
|
67
|
+
// validation then refuses it (`eq` is not a range op) instead of reading `now` as an op.
|
|
68
|
+
if (typeof raw === "object") {
|
|
69
|
+
return FILTER_RELATIVE_NOW in raw ? { [FILTER_OP_EQ]: raw } : (raw as AttrObject);
|
|
70
|
+
}
|
|
64
71
|
return { [FILTER_OP_EQ]: raw };
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -49,6 +49,22 @@ const BASE_ATTR_DATA_TYPE: Readonly<Record<string, DataType>> = {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
export class MetaAttr extends MetaData implements DataTypeAware {
|
|
52
|
+
/**
|
|
53
|
+
* ADR-0059 R5 — for a reference attribute, the top-level object each reference
|
|
54
|
+
* names, set once by the loader's `resolveReferences` pass. A scalar value holds
|
|
55
|
+
* one node; a list value holds one entry per element (`undefined` where that
|
|
56
|
+
* element did not resolve). Unset when the attr is not a reference or did not
|
|
57
|
+
* resolve.
|
|
58
|
+
*/
|
|
59
|
+
resolvedTarget?: MetaData | readonly (MetaData | undefined)[];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ADR-0059 R5 — the value exactly as authored, kept when `resolveReferences`
|
|
63
|
+
* rewrites `value` to its canonical spelling. `meta fmt` writes this back
|
|
64
|
+
* (`refs: "authored"`). Unset when the value was never rewritten.
|
|
65
|
+
*/
|
|
66
|
+
authoredValue?: AttrValue;
|
|
67
|
+
|
|
52
68
|
/** This attribute's coerced value (stored on the instance under RESERVED_KEY_VALUE). */
|
|
53
69
|
get value(): AttrValue | undefined {
|
|
54
70
|
// ADR-0039: own — an attr node's own scalar payload (the RESERVED_KEY_VALUE
|
|
@@ -95,7 +111,14 @@ export class MetaAttr extends MetaData implements DataTypeAware {
|
|
|
95
111
|
if (dt === DATA_TYPE_STRING) {
|
|
96
112
|
return typeof value === "string" ? [] : [this._typeError("string", value)];
|
|
97
113
|
}
|
|
98
|
-
if (dt === DATA_TYPE_INT || dt === DATA_TYPE_LONG
|
|
114
|
+
if (dt === DATA_TYPE_INT || dt === DATA_TYPE_LONG) {
|
|
115
|
+
if (typeof value !== "number") return [this._typeError("number", value)];
|
|
116
|
+
// Rule A: an integral number (4, 4.0, 1e2) is an int; a fraction is not.
|
|
117
|
+
return Number.isInteger(value)
|
|
118
|
+
? []
|
|
119
|
+
: [{ message: `attribute '@${this.name}' must be of type '${this.subType}' but got the fraction ${value}; this attribute is integral, so write an integer (for a validator @min/@max, use an integer bound)` }];
|
|
120
|
+
}
|
|
121
|
+
if (dt === DATA_TYPE_DOUBLE) {
|
|
99
122
|
return typeof value === "number" ? [] : [this._typeError("number", value)];
|
|
100
123
|
}
|
|
101
124
|
if (dt === DATA_TYPE_BOOLEAN) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Shared enum declarations — the single authority for "which type does this enum
|
|
2
|
+
// field have" (FR-019, ADR-0026 Amendment 1). The loader's #246 check and every
|
|
3
|
+
// codegen site that names, references, imports or materializes an enum type call
|
|
4
|
+
// these two functions; neither keeps a predicate of its own.
|
|
5
|
+
|
|
6
|
+
import { TYPE_FIELD, TYPE_METADATA } from "../../shared/base-types.js";
|
|
7
|
+
import type { MetaData } from "../../shared/meta-data.js";
|
|
8
|
+
import { FIELD_ATTR_INT_VALUE_MAP, FIELD_ATTR_VALUES, FIELD_SUBTYPE_ENUM } from "./field-constants.js";
|
|
9
|
+
import type { MetaField } from "./meta-field.js";
|
|
10
|
+
|
|
11
|
+
function isEnumField(node: MetaData | undefined): node is MetaField {
|
|
12
|
+
return node !== undefined && node.type === TYPE_FIELD && node.subType === FIELD_SUBTYPE_ENUM;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* True when `node` is a shared enum declaration: an abstract `field.enum` that is a
|
|
17
|
+
* direct child of the metadata root (a package-level sibling of `object.entity`). An
|
|
18
|
+
* abstract enum nested inside an object is not one.
|
|
19
|
+
*/
|
|
20
|
+
export function isSharedEnumDeclaration(node: MetaData | undefined): node is MetaField {
|
|
21
|
+
return isEnumField(node) && isRootAbstract(node);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRootAbstract(field: MetaField): boolean {
|
|
25
|
+
return field.isAbstract && field.parent?.type === TYPE_METADATA;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The shared enum declaration an enum field's type resolves to, or undefined when the
|
|
30
|
+
* field keeps a per-object inline type.
|
|
31
|
+
*
|
|
32
|
+
* Walks from the field itself up its `extends` chain. At each node, in order: a shared
|
|
33
|
+
* declaration answers (the nearest one wins, so `Money extends Currency` answers
|
|
34
|
+
* `Money`); a node that declares its own `@values` or `@intValueMap` starts a new
|
|
35
|
+
* vocabulary and answers undefined; otherwise the walk continues at the super. A
|
|
36
|
+
* non-enum node, the end of the chain or a cycle answers undefined. The hosting
|
|
37
|
+
* object's subtype and the number of hops play no part.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveSharedEnumDeclaration(field: MetaData | undefined): MetaField | undefined {
|
|
40
|
+
const seen = new Set<MetaData>();
|
|
41
|
+
let node = field;
|
|
42
|
+
while (isEnumField(node) && !seen.has(node)) {
|
|
43
|
+
if (isRootAbstract(node)) return node;
|
|
44
|
+
// ADR-0039 own (declared-here layer): the question is whether THIS node redeclares
|
|
45
|
+
// the vocabulary. A resolving read would see the inherited @values on every hop and
|
|
46
|
+
// stop the walk at the first one.
|
|
47
|
+
const own = node.ownAttrs();
|
|
48
|
+
if (own.has(FIELD_ATTR_VALUES) || own.has(FIELD_ATTR_INT_VALUE_MAP)) return undefined;
|
|
49
|
+
seen.add(node);
|
|
50
|
+
node = node.superData;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
IDENTITY_REFERENCE_ATTR_ON_UPDATE,
|
|
17
17
|
} from "./identity-constants.js";
|
|
18
18
|
import type { MetaRoot } from "../../shared/meta-root.js";
|
|
19
|
+
import type { MetaObject } from "../object/meta-object.js";
|
|
20
|
+
import { TYPE_OBJECT } from "../../shared/base-types.js";
|
|
21
|
+
import { refTarget } from "../../naming-refs.js";
|
|
19
22
|
|
|
20
23
|
/** Strongly-typed identity generation strategies. */
|
|
21
24
|
export type IdentityGeneration = "increment" | "uuid" | "assigned";
|
|
@@ -144,7 +147,7 @@ export class MetaReferenceIdentity extends MetaIdentity {
|
|
|
144
147
|
* points at. Priority: explicit dotted-form `@references: "Entity.field"`
|
|
145
148
|
* override → target entity's primary identity field → "id" fallback.
|
|
146
149
|
*
|
|
147
|
-
* Returns undefined only if the target entity
|
|
150
|
+
* Returns undefined only if the target entity does not resolve.
|
|
148
151
|
* Centralizes the FK-target resolution rule used by projection view DDL,
|
|
149
152
|
* Drizzle schema emit, and migration schema comparison.
|
|
150
153
|
*/
|
|
@@ -152,17 +155,11 @@ export class MetaReferenceIdentity extends MetaIdentity {
|
|
|
152
155
|
const explicit = this.targetFields[0];
|
|
153
156
|
if (explicit !== undefined) return explicit;
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// target and the PK column wrongly defaults to "id".
|
|
161
|
-
const targetObj = root.findObject(targetName)
|
|
162
|
-
?? (targetName.includes("::")
|
|
163
|
-
? root.findObject(targetName.slice(targetName.lastIndexOf("::") + 2))
|
|
164
|
-
: undefined);
|
|
165
|
-
if (!targetObj) return undefined;
|
|
158
|
+
// ADR-0059 R7: the target the loader bound for @references, else (a tree built
|
|
159
|
+
// without the loader) the value resolved from this reference's package.
|
|
160
|
+
const bound = refTarget(this, IDENTITY_REFERENCE_ATTR_REFERENCES);
|
|
161
|
+
if (bound === undefined || bound.type !== TYPE_OBJECT) return undefined;
|
|
162
|
+
const targetObj = bound as MetaObject;
|
|
166
163
|
|
|
167
164
|
const primary = targetObj.primaryIdentity();
|
|
168
165
|
// ADR-0039: resolving — the target's @fields may be inherited via extends.
|
package/src/core/index.ts
CHANGED
|
@@ -16,3 +16,8 @@ export { UriSource } from "../loader/sources/uri-source.js";
|
|
|
16
16
|
export { parseYaml } from "./parser-yaml.js";
|
|
17
17
|
export { loadAndExportJson } from "./export-json.js";
|
|
18
18
|
export type { ExportResult } from "./export-json.js";
|
|
19
|
+
|
|
20
|
+
// The `meta verify` field lint's structural pre-parse walk. Lives here, not on the
|
|
21
|
+
// root entry, because it parses YAML.
|
|
22
|
+
export { declaredDuplicateFields } from "../loader/declared-duplicate-fields.js";
|
|
23
|
+
export type { DeclaredDuplicateField } from "../loader/declared-duplicate-fields.js";
|
|
@@ -14,12 +14,15 @@ import { SUBTYPE_BASE } from "../../shared/base-types.js";
|
|
|
14
14
|
export const OBJECT_SUBTYPE_ENTITY = "entity";
|
|
15
15
|
export const OBJECT_SUBTYPE_VALUE = "value";
|
|
16
16
|
export const OBJECT_SUBTYPE_PROJECTION = "projection";
|
|
17
|
+
// - report : declared dimensions x measures of one entity, compiled to a read-only view (FR-044)
|
|
18
|
+
export const OBJECT_SUBTYPE_REPORT = "report";
|
|
17
19
|
|
|
18
20
|
export const OBJECT_SUBTYPES = [
|
|
19
21
|
SUBTYPE_BASE,
|
|
20
22
|
OBJECT_SUBTYPE_ENTITY,
|
|
21
23
|
OBJECT_SUBTYPE_VALUE,
|
|
22
24
|
OBJECT_SUBTYPE_PROJECTION,
|
|
25
|
+
OBJECT_SUBTYPE_REPORT,
|
|
23
26
|
] as const;
|
|
24
27
|
export type ObjectSubType = (typeof OBJECT_SUBTYPES)[number];
|
|
25
28
|
|
|
@@ -49,3 +52,19 @@ export const OBJECT_ATTR_DISCRIMINATOR_VALUE = "discriminatorValue";
|
|
|
49
52
|
* but resolves against the projection's OWN declared fields rather than one
|
|
50
53
|
* aggregated entity. An aggregate-derived field is not addressable (fail-closed). */
|
|
51
54
|
export const OBJECT_PROJECTION_ATTR_FILTER = "filter";
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// FR-044 — object.report attrs
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/** The object.entity whose rows the report aggregates. */
|
|
60
|
+
export const OBJECT_REPORT_ATTR_FROM = "from";
|
|
61
|
+
/** Dimension names of @from; a time dimension is written `name:grain`. */
|
|
62
|
+
export const OBJECT_REPORT_ATTR_DIMENSIONS = "dimensions";
|
|
63
|
+
/** Measure names of @from. */
|
|
64
|
+
export const OBJECT_REPORT_ATTR_MEASURES = "measures";
|
|
65
|
+
/** Optional segment of @from scoping the rows. */
|
|
66
|
+
export const OBJECT_REPORT_ATTR_SEGMENT = "segment";
|
|
67
|
+
/** Optional row scope over @from's fields (an attr.filter). */
|
|
68
|
+
export const OBJECT_REPORT_ATTR_FILTER = "filter";
|
|
69
|
+
/** Optional to-one path from @from to the entity whose rows supply the report's rows (R8). */
|
|
70
|
+
export const OBJECT_REPORT_ATTR_SPINE = "spine";
|
|
@@ -80,6 +80,27 @@ export const OBJECT_DEFINITION: ProviderDefinition = {
|
|
|
80
80
|
"min": 0,
|
|
81
81
|
"max": null
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
"type": "dimension",
|
|
85
|
+
"subType": "*",
|
|
86
|
+
"name": "*",
|
|
87
|
+
"min": 0,
|
|
88
|
+
"max": null
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"type": "measure",
|
|
92
|
+
"subType": "*",
|
|
93
|
+
"name": "*",
|
|
94
|
+
"min": 0,
|
|
95
|
+
"max": null
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "segment",
|
|
99
|
+
"subType": "*",
|
|
100
|
+
"name": "*",
|
|
101
|
+
"min": 0,
|
|
102
|
+
"max": null
|
|
103
|
+
},
|
|
83
104
|
{
|
|
84
105
|
"type": "attr",
|
|
85
106
|
"subType": "string",
|
|
@@ -132,6 +153,65 @@ export const OBJECT_DEFINITION: ProviderDefinition = {
|
|
|
132
153
|
"description": "Optional row-scope predicate (a portable attr.filter object: eq/ne/gt/gte/lt/lte/like/in/isNull with and/or, desugared to { field: { op: value } } at parse time) selecting which rows the view returns — lowered to an outer SQL WHERE. Resolves against the projection's own declared fields; an aggregate-derived field is not addressable (fail-closed)."
|
|
133
154
|
}
|
|
134
155
|
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"type": "object",
|
|
159
|
+
"subType": "report",
|
|
160
|
+
"extendsBase": true,
|
|
161
|
+
"description": "A declared report (FR-044): a fixed combination of dimensions and measures of ONE entity (@from), compiled to a read-only view. One row per distinct dimension tuple; no dimensions means exactly one row. Fields are DERIVED, not declared: one per dimension (a time dimension's field is <dimension><Grain>, e.g. purchasedAtDay) and one per measure. Every measure must belong to @from (two fact tables are two reports). @filter and @segment scope rows before grouping and combine by AND. Read-only: no writes, no get-by-id.",
|
|
162
|
+
"whenToUse": "Dashboard totals, per-day series and per-group summaries that would otherwise be hand-written GROUP BY queries.",
|
|
163
|
+
"children": [
|
|
164
|
+
{
|
|
165
|
+
"type": "attr",
|
|
166
|
+
"subType": "string",
|
|
167
|
+
"name": "from",
|
|
168
|
+
"min": 1,
|
|
169
|
+
"max": 1,
|
|
170
|
+
"description": "The object.entity whose rows the report aggregates."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "attr",
|
|
174
|
+
"subType": "string",
|
|
175
|
+
"name": "dimensions",
|
|
176
|
+
"isArray": true,
|
|
177
|
+
"min": 0,
|
|
178
|
+
"max": 1,
|
|
179
|
+
"description": "Dimension names of @from; a time dimension is written 'name:grain'. Absent means one global row."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "attr",
|
|
183
|
+
"subType": "string",
|
|
184
|
+
"name": "measures",
|
|
185
|
+
"isArray": true,
|
|
186
|
+
"min": 1,
|
|
187
|
+
"max": 1,
|
|
188
|
+
"description": "Measure names of @from."
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"type": "attr",
|
|
192
|
+
"subType": "string",
|
|
193
|
+
"name": "segment",
|
|
194
|
+
"min": 0,
|
|
195
|
+
"max": 1,
|
|
196
|
+
"description": "Optional segment of @from scoping the rows."
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"type": "attr",
|
|
200
|
+
"subType": "filter",
|
|
201
|
+
"name": "filter",
|
|
202
|
+
"min": 0,
|
|
203
|
+
"max": 1,
|
|
204
|
+
"description": "Optional row scope over @from's fields; may use relative-date values."
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"type": "attr",
|
|
208
|
+
"subType": "string",
|
|
209
|
+
"name": "spine",
|
|
210
|
+
"min": 0,
|
|
211
|
+
"max": 1,
|
|
212
|
+
"description": "Optional to-one path from @from to the entity whose rows supply the report's rows (e.g. 'Purchase.program'), written like a dimension's @via. With it the report has one row per distinct dimension tuple among THAT entity's rows, including the ones no row of @from refers to: a count there is 0 and any other measure is null unless it declares @default. Every listed dimension must be reached through this path. @segment and @filter still scope the rows of @from and never remove a row. A row of @from whose reference is null or matches nothing is in no row of the report."
|
|
213
|
+
}
|
|
214
|
+
]
|
|
135
215
|
}
|
|
136
216
|
]
|
|
137
217
|
};
|
|
@@ -44,7 +44,9 @@ import type { MetaRoot } from "../../shared/meta-root.js";
|
|
|
44
44
|
import type { MetaRelationship } from "./meta-relationship.js";
|
|
45
45
|
import type { MetaReferenceIdentity } from "../identity/meta-identity.js";
|
|
46
46
|
import { stripPackage } from "../../naming.js";
|
|
47
|
-
import {
|
|
47
|
+
import { refTarget } from "../../naming-refs.js";
|
|
48
|
+
import { IDENTITY_REFERENCE_ATTR_REFERENCES } from "../identity/identity-constants.js";
|
|
49
|
+
import { RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_THROUGH } from "./relationship-constants.js";
|
|
48
50
|
import { TYPE_OBJECT } from "../../shared/base-types.js";
|
|
49
51
|
import { PACKAGE_SEPARATOR } from "../../shared/structural.js";
|
|
50
52
|
|
|
@@ -69,37 +71,6 @@ function refFkField(ref: MetaReferenceIdentity): string | undefined {
|
|
|
69
71
|
return ref.fields[0];
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
/**
|
|
73
|
-
* The root entity a reference name denotes, or undefined — resolved by the ONE
|
|
74
|
-
* matcher the loader uses (`resolveObjectRef`, ADR-0041/0042): a fully-qualified
|
|
75
|
-
* name binds exactly on its resolution key, a bare name binds in the REFERRER's
|
|
76
|
-
* package.
|
|
77
|
-
*
|
|
78
|
-
* This exists so the SUBJECT comparison below can be made on object IDENTITY.
|
|
79
|
-
* A bare-name compare cannot tell `a::NodeBase` from `b::NodeBase`, which made a
|
|
80
|
-
* genuine cross-package hetero M:N read as a self-join the moment the subject
|
|
81
|
-
* set held two names — and the first-match-wins bare arm this used to carry
|
|
82
|
-
* (Java's deferred #174) bound a bare name to whichever same-short-named entity
|
|
83
|
-
* happened to load first, which is load-ORDER dependence, not a naming rule.
|
|
84
|
-
*/
|
|
85
|
-
function findEntity(
|
|
86
|
-
root: MetaRoot,
|
|
87
|
-
name: string | undefined,
|
|
88
|
-
referrerPkg: string,
|
|
89
|
-
): MetaObject | undefined {
|
|
90
|
-
if (name === undefined || name === "") return undefined;
|
|
91
|
-
// ADR-0041/0042 through the single resolveObjectRef matcher — an FQN resolves
|
|
92
|
-
// exactly on its resolution key, a BARE name resolves in the REFERRER's
|
|
93
|
-
// package. The bare arm used to be `objects().find(o => o.name === bare)`,
|
|
94
|
-
// which took whichever same-short-named entity loaded first: a bare
|
|
95
|
-
// `@objectRef: "Tag"` on an abstract base in package `base` bound to
|
|
96
|
-
// `acme::Tag` merely because the acme source was read first. Callers pass the
|
|
97
|
-
// referrer that owns the name they are resolving — the DECLARING entity for
|
|
98
|
-
// the relationship's own @objectRef, the JUNCTION for its references' —
|
|
99
|
-
// because those are different packages whenever a base is inherited across one.
|
|
100
|
-
return resolveObjectRef(root, name, referrerPkg).node as MetaObject | undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
74
|
/**
|
|
104
75
|
* Derive the source/target junction FK fields for a M:N relationship.
|
|
105
76
|
*
|
|
@@ -108,7 +79,7 @@ function findEntity(
|
|
|
108
79
|
* @param source the entity the caller is navigating from. Accepted alongside
|
|
109
80
|
* `rel.parent` as a name for the relationship's subject, and used
|
|
110
81
|
* as the declaring entity when `rel` has no entity parent.
|
|
111
|
-
* @param root the loaded model root
|
|
82
|
+
* @param root the loaded model root
|
|
112
83
|
* @throws M2MDerivationError when the junction is missing/malformed or the
|
|
113
84
|
* self-join is ambiguous.
|
|
114
85
|
*/
|
|
@@ -134,9 +105,10 @@ export function deriveM2MFields(
|
|
|
134
105
|
`relationship "${declaringEntity.name}.${rel.name}" is missing @through (required for M:N derivation)`,
|
|
135
106
|
);
|
|
136
107
|
}
|
|
137
|
-
// ADR-
|
|
138
|
-
// (
|
|
139
|
-
//
|
|
108
|
+
// ADR-0059 R7 — @through, @objectRef and the junction's @references are read
|
|
109
|
+
// as bound targets (refTarget), each resolved in the package of the node that
|
|
110
|
+
// declares it, so an inherited reference keeps the meaning it had where it was
|
|
111
|
+
// written.
|
|
140
112
|
//
|
|
141
113
|
// This used to be `root.findObject(fqn)` with a fall back to the bare suffix
|
|
142
114
|
// after the last "::". findObject is keyed by BARE name, so the FQN lookup
|
|
@@ -147,12 +119,7 @@ export function deriveM2MFields(
|
|
|
147
119
|
// nowhere near the navigating entity. The pairing then failed, so a valid
|
|
148
120
|
// cross-package M:N emitted no traversal route in TS and C# and failed the
|
|
149
121
|
// build in Java, Kotlin and Python.
|
|
150
|
-
|
|
151
|
-
// Note findEntity() below was ALREADY FQN-exact for the same reason (see its
|
|
152
|
-
// doc comment). The two halves of one function disagreed about how a
|
|
153
|
-
// package-qualified name resolves; only the junction half was wrong.
|
|
154
|
-
const referrerPkg = declaringEntity.package ?? declaringEntity.fileDefaultPackage ?? "";
|
|
155
|
-
const junction = resolveObjectRef(root, throughName, referrerPkg).node as MetaObject | undefined;
|
|
122
|
+
const junction = refTarget(rel, RELATIONSHIP_ATTR_THROUGH) as MetaObject | undefined;
|
|
156
123
|
if (junction === undefined) {
|
|
157
124
|
throw new M2MDerivationError(
|
|
158
125
|
`relationship "${declaringEntity.name}.${rel.name}" @through "${throughName}" does not resolve to an entity`,
|
|
@@ -202,16 +169,16 @@ export function deriveM2MFields(
|
|
|
202
169
|
name !== undefined && subjectNames.includes(stripPackage(name));
|
|
203
170
|
|
|
204
171
|
// The junction owns its references' @references names, so those resolve in the
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// one for both is what bound a bare @objectRef to the wrong
|
|
208
|
-
|
|
172
|
+
// package of the node that declares them; the relationship's own @objectRef in
|
|
173
|
+
// the package of the node that declares it. Under `extends` across packages these
|
|
174
|
+
// differ, and using one for both is what bound a bare @objectRef to the wrong
|
|
175
|
+
// same-named entity.
|
|
209
176
|
const refEntity = (r: MetaReferenceIdentity): MetaObject | undefined =>
|
|
210
|
-
|
|
177
|
+
refTarget(r, IDENTITY_REFERENCE_ATTR_REFERENCES) as MetaObject | undefined;
|
|
211
178
|
|
|
212
179
|
// Defensive bare fallback when @objectRef does not resolve — loader validation
|
|
213
180
|
// normally guarantees it does. Same carve-out the Java reference makes.
|
|
214
|
-
const targetEntityNode =
|
|
181
|
+
const targetEntityNode = refTarget(rel, RELATIONSHIP_ATTR_OBJECT_REF) as MetaObject | undefined;
|
|
215
182
|
const isSelfJoin = targetEntityNode !== undefined
|
|
216
183
|
? isSubject(targetEntityNode)
|
|
217
184
|
: isSubjectName(targetName);
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import type { MetaObject } from "../object/meta-object.js";
|
|
10
10
|
import type { MetaReferenceIdentity } from "../identity/meta-identity.js";
|
|
11
|
-
import {
|
|
11
|
+
import { refTarget } from "../../naming-refs.js";
|
|
12
|
+
import { IDENTITY_REFERENCE_ATTR_REFERENCES } from "../identity/identity-constants.js";
|
|
12
13
|
|
|
13
14
|
export interface ReferenceLookup {
|
|
14
15
|
/** The entity whose identity.reference points at `other`. */
|
|
@@ -39,7 +40,8 @@ export function findReferencesBetween(
|
|
|
39
40
|
const found: ReferenceLookup[] = [];
|
|
40
41
|
for (const [holder, other] of [[a, b], [b, a]] as const) {
|
|
41
42
|
for (const ref of holder.referenceIdentities()) {
|
|
42
|
-
|
|
43
|
+
// ADR-0059 R7: compare the bound target, never a short name.
|
|
44
|
+
if (refTarget(ref, IDENTITY_REFERENCE_ATTR_REFERENCES) === other) {
|
|
43
45
|
found.push({ holder, other, referenceIdentity: ref });
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import type { MetaObject } from "../object/meta-object.js";
|
|
12
12
|
import type { MetaReferenceIdentity } from "../identity/meta-identity.js";
|
|
13
|
-
import {
|
|
13
|
+
import type { MetaData } from "../../shared/meta-data.js";
|
|
14
|
+
import { refTarget } from "../../naming-refs.js";
|
|
15
|
+
import { IDENTITY_REFERENCE_ATTR_REFERENCES } from "../identity/identity-constants.js";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Trailing suffixes stripped from a CANDIDATE's name/FK field when building its
|
|
@@ -51,20 +53,20 @@ export function referencePairingKeys(ref: MetaReferenceIdentity): Set<string> {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
* Every identity.reference on `holder` whose @references targets `targetEntity
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* Every identity.reference on `holder` whose @references targets `targetEntity`: a
|
|
57
|
+
* reference matches when its @references is bound to that node (ADR-0059 R7: identity,
|
|
58
|
+
* never a short name). An unresolved target (undefined) matches nothing.
|
|
57
59
|
*/
|
|
58
60
|
export function referenceCandidatesFor(
|
|
59
61
|
holder: MetaObject,
|
|
60
|
-
targetEntity:
|
|
62
|
+
targetEntity: MetaData | undefined,
|
|
61
63
|
): MetaReferenceIdentity[] {
|
|
62
|
-
|
|
64
|
+
if (targetEntity === undefined) return [];
|
|
63
65
|
// ADR-0039: resolving — referenceIdentities() honors references inherited via extends.
|
|
64
66
|
return holder
|
|
65
67
|
.referenceIdentities()
|
|
66
|
-
.filter((ref) =>
|
|
67
|
-
.filter((ref) =>
|
|
68
|
+
.filter((ref) => refFkField(ref) !== undefined)
|
|
69
|
+
.filter((ref) => refTarget(ref, IDENTITY_REFERENCE_ATTR_REFERENCES) === targetEntity);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -82,7 +84,7 @@ export function referenceCandidatesFor(
|
|
|
82
84
|
export function resolveRelationshipReference(
|
|
83
85
|
holder: MetaObject,
|
|
84
86
|
relationshipName: string,
|
|
85
|
-
targetEntity:
|
|
87
|
+
targetEntity: MetaData | undefined,
|
|
86
88
|
sourceRefField?: string,
|
|
87
89
|
): MetaReferenceIdentity | undefined {
|
|
88
90
|
const candidates = referenceCandidatesFor(holder, targetEntity);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// MetaDimension — concrete node class for type=dimension nodes (FR-044).
|
|
2
|
+
//
|
|
3
|
+
// Extends MetaData directly. Accessors are RESOLVING (ADR-0039): a dimension
|
|
4
|
+
// declared on an abstract base entity is read through the same accessors as one
|
|
5
|
+
// declared on the concrete entity.
|
|
6
|
+
|
|
7
|
+
import { MetaData } from "../../shared/meta-data.js";
|
|
8
|
+
import {
|
|
9
|
+
DIMENSION_SUBTYPE_TIME,
|
|
10
|
+
REPORTING_ATTR_GRAINS,
|
|
11
|
+
REPORTING_ATTR_OF,
|
|
12
|
+
REPORTING_ATTR_VIA,
|
|
13
|
+
TIME_GRAINS,
|
|
14
|
+
type TimeGrain,
|
|
15
|
+
} from "./reporting-constants.js";
|
|
16
|
+
|
|
17
|
+
export class MetaDimension extends MetaData {
|
|
18
|
+
/** True for `dimension.time` (grain truncation); false for `dimension.attribute`. */
|
|
19
|
+
isTime(): boolean {
|
|
20
|
+
return this.subType === DIMENSION_SUBTYPE_TIME;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Dotted `Entity.field` reference naming the grouped column. */
|
|
24
|
+
of(): string | undefined {
|
|
25
|
+
const v = this.attr(REPORTING_ATTR_OF);
|
|
26
|
+
return typeof v === "string" ? v : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Optional dotted to-one relationship path from the owning entity to the `@of` entity. */
|
|
30
|
+
via(): string | undefined {
|
|
31
|
+
const v = this.attr(REPORTING_ATTR_VIA);
|
|
32
|
+
return typeof v === "string" ? v : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The grains a `dimension.time` supports (empty for `dimension.attribute`). */
|
|
36
|
+
grains(): TimeGrain[] {
|
|
37
|
+
const v = this.attr(REPORTING_ATTR_GRAINS);
|
|
38
|
+
const items = Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
|
|
39
|
+
return items.filter((g): g is TimeGrain => (TIME_GRAINS as readonly unknown[]).includes(g));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// MetaMeasure — concrete node class for type=measure nodes (FR-044).
|
|
2
|
+
//
|
|
3
|
+
// Extends MetaData directly. Accessors are RESOLVING (ADR-0039).
|
|
4
|
+
|
|
5
|
+
import { MetaData } from "../../shared/meta-data.js";
|
|
6
|
+
import {
|
|
7
|
+
MEASURE_AGGS,
|
|
8
|
+
MEASURE_SUBTYPE_RATIO,
|
|
9
|
+
REPORTING_ATTR_AGG,
|
|
10
|
+
REPORTING_ATTR_DEFAULT,
|
|
11
|
+
REPORTING_ATTR_DENOMINATOR,
|
|
12
|
+
REPORTING_ATTR_DISTINCT,
|
|
13
|
+
REPORTING_ATTR_FILTER,
|
|
14
|
+
REPORTING_ATTR_NUMERATOR,
|
|
15
|
+
REPORTING_ATTR_OF,
|
|
16
|
+
REPORTING_ATTR_SEGMENT,
|
|
17
|
+
type MeasureAgg,
|
|
18
|
+
} from "./reporting-constants.js";
|
|
19
|
+
|
|
20
|
+
function optionalString(v: unknown): string | undefined {
|
|
21
|
+
return typeof v === "string" ? v : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class MetaMeasure extends MetaData {
|
|
25
|
+
/** True for `measure.ratio`; false for `measure.aggregate`. */
|
|
26
|
+
isRatio(): boolean {
|
|
27
|
+
return this.subType === MEASURE_SUBTYPE_RATIO;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The aggregate function (`measure.aggregate` only). */
|
|
31
|
+
agg(): MeasureAgg | undefined {
|
|
32
|
+
const v = this.attr(REPORTING_ATTR_AGG);
|
|
33
|
+
return (MEASURE_AGGS as readonly unknown[]).includes(v) ? (v as MeasureAgg) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when `@distinct` is set (legal only with `@agg: count`). */
|
|
37
|
+
distinct(): boolean {
|
|
38
|
+
return this.attr(REPORTING_ATTR_DISTINCT) === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The `Entity.field` references in `@of`: a bare string is one column, a list is the tuple form. */
|
|
42
|
+
ofColumns(): string[] {
|
|
43
|
+
const v = this.attr(REPORTING_ATTR_OF);
|
|
44
|
+
if (Array.isArray(v)) return v.filter((x): x is string => typeof x === "string");
|
|
45
|
+
return typeof v === "string" ? [v] : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Name of a segment declared on the same entity; combines with `@filter` by AND. */
|
|
49
|
+
segmentName(): string | undefined {
|
|
50
|
+
return optionalString(this.attr(REPORTING_ATTR_SEGMENT));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The canonical row-scope filter, when one is declared. */
|
|
54
|
+
filter(): Record<string, unknown> | undefined {
|
|
55
|
+
const v = this.attr(REPORTING_ATTR_FILTER);
|
|
56
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `@default`: the integer this measure reads when it would otherwise be null (rule M7/M8
|
|
60
|
+
* decide where it is legal). ADR-0039: resolving, so an inherited measure keeps it. */
|
|
61
|
+
defaultValue(): number | undefined {
|
|
62
|
+
const v = this.attr(REPORTING_ATTR_DEFAULT);
|
|
63
|
+
return typeof v === "number" && Number.isInteger(v) ? v : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Name of the `measure.aggregate` sibling used as the numerator (`measure.ratio` only). */
|
|
67
|
+
numerator(): string | undefined {
|
|
68
|
+
return optionalString(this.attr(REPORTING_ATTR_NUMERATOR));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Name of the `measure.aggregate` sibling used as the denominator (`measure.ratio` only). */
|
|
72
|
+
denominator(): string | undefined {
|
|
73
|
+
return optionalString(this.attr(REPORTING_ATTR_DENOMINATOR));
|
|
74
|
+
}
|
|
75
|
+
}
|