@metaobjectsdev/metadata 0.8.1-rc.1 → 0.9.0-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/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +49 -1
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/field/field-constants.d.ts +27 -2
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +30 -2
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-schema.d.ts +9 -1
- package/dist/core/field/field-schema.d.ts.map +1 -1
- package/dist/core/field/field-schema.js +34 -3
- package/dist/core/field/field-schema.js.map +1 -1
- package/dist/core/field/meta-field.d.ts +10 -0
- package/dist/core/field/meta-field.d.ts.map +1 -1
- package/dist/core/field/meta-field.js +36 -1
- package/dist/core/field/meta-field.js.map +1 -1
- package/dist/core/field/validate-field-readonly.d.ts +9 -0
- package/dist/core/field/validate-field-readonly.d.ts.map +1 -0
- package/dist/core/field/validate-field-readonly.js +116 -0
- package/dist/core/field/validate-field-readonly.js.map +1 -0
- package/dist/core/object/meta-object-aware.d.ts +11 -0
- package/dist/core/object/meta-object-aware.d.ts.map +1 -0
- package/dist/core/object/meta-object-aware.js +15 -0
- package/dist/core/object/meta-object-aware.js.map +1 -0
- package/dist/core/object/meta-object.d.ts +21 -0
- package/dist/core/object/meta-object.d.ts.map +1 -1
- package/dist/core/object/meta-object.js +43 -2
- package/dist/core/object/meta-object.js.map +1 -1
- package/dist/core/object/object-class-registry.d.ts +22 -0
- package/dist/core/object/object-class-registry.d.ts.map +1 -0
- package/dist/core/object/object-class-registry.js +38 -0
- package/dist/core/object/object-class-registry.js.map +1 -0
- package/dist/core/object/object-constants.d.ts +7 -0
- package/dist/core/object/object-constants.d.ts.map +1 -1
- package/dist/core/object/object-constants.js +14 -0
- package/dist/core/object/object-constants.js.map +1 -1
- package/dist/core/object/object-schema.d.ts.map +1 -1
- package/dist/core/object/object-schema.js +24 -1
- package/dist/core/object/object-schema.js.map +1 -1
- package/dist/core/object/validate-discriminator.d.ts +4 -0
- package/dist/core/object/validate-discriminator.d.ts.map +1 -0
- package/dist/core/object/validate-discriminator.js +145 -0
- package/dist/core/object/validate-discriminator.js.map +1 -0
- package/dist/core/object/value-object.d.ts +23 -0
- package/dist/core/object/value-object.d.ts.map +1 -0
- package/dist/core/object/value-object.js +51 -0
- package/dist/core/object/value-object.js.map +1 -0
- package/dist/core/query/query-constants.d.ts.map +1 -1
- package/dist/core/query/query-constants.js +4 -0
- package/dist/core/query/query-constants.js.map +1 -1
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +16 -4
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +26 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +122 -2
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +7 -6
- package/dist/naming.js.map +1 -1
- package/dist/persistence/db/db-constants.d.ts +23 -0
- package/dist/persistence/db/db-constants.d.ts.map +1 -1
- package/dist/persistence/db/db-constants.js +31 -0
- package/dist/persistence/db/db-constants.js.map +1 -1
- package/dist/persistence/db/db-provider.js +3 -3
- package/dist/persistence/db/db-provider.js.map +1 -1
- package/dist/persistence/db/db-schema.d.ts +8 -0
- package/dist/persistence/db/db-schema.d.ts.map +1 -1
- package/dist/persistence/db/db-schema.js +17 -1
- package/dist/persistence/db/db-schema.js.map +1 -1
- package/dist/persistence/source/meta-source.d.ts +15 -1
- package/dist/persistence/source/meta-source.d.ts.map +1 -1
- package/dist/persistence/source/meta-source.js +55 -3
- package/dist/persistence/source/meta-source.js.map +1 -1
- package/dist/persistence/source/source-constants.d.ts +20 -1
- package/dist/persistence/source/source-constants.d.ts.map +1 -1
- package/dist/persistence/source/source-constants.js +36 -1
- package/dist/persistence/source/source-constants.js.map +1 -1
- package/dist/persistence/source/source-schema.d.ts.map +1 -1
- package/dist/persistence/source/source-schema.js +65 -4
- package/dist/persistence/source/source-schema.js.map +1 -1
- package/dist/persistence/source/validate-source-parameter-ref.d.ts +4 -0
- package/dist/persistence/source/validate-source-parameter-ref.d.ts.map +1 -0
- package/dist/persistence/source/validate-source-parameter-ref.js +96 -0
- package/dist/persistence/source/validate-source-parameter-ref.js.map +1 -0
- package/dist/persistence/source/validate-source-physical-names.d.ts +9 -0
- package/dist/persistence/source/validate-source-physical-names.d.ts.map +1 -0
- package/dist/persistence/source/validate-source-physical-names.js +79 -0
- package/dist/persistence/source/validate-source-physical-names.js.map +1 -0
- package/dist/serializer-json.d.ts.map +1 -1
- package/dist/serializer-json.js +43 -0
- package/dist/serializer-json.js.map +1 -1
- package/dist/template/template-constants.d.ts +9 -0
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +20 -0
- package/dist/template/template-constants.js.map +1 -1
- package/dist/template/template-schema.d.ts.map +1 -1
- package/dist/template/template-schema.js +42 -3
- package/dist/template/template-schema.js.map +1 -1
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +70 -0
- package/src/core/field/field-constants.ts +37 -2
- package/src/core/field/field-schema.ts +43 -2
- package/src/core/field/meta-field.ts +39 -0
- package/src/core/field/validate-field-readonly.ts +142 -0
- package/src/core/object/meta-object-aware.ts +27 -0
- package/src/core/object/meta-object.ts +47 -2
- package/src/core/object/object-class-registry.ts +48 -0
- package/src/core/object/object-constants.ts +17 -0
- package/src/core/object/object-schema.ts +29 -1
- package/src/core/object/validate-discriminator.ts +195 -0
- package/src/core/object/value-object.ts +65 -0
- package/src/core/query/query-constants.ts +5 -0
- package/src/core-types.ts +17 -4
- package/src/errors.ts +23 -0
- package/src/index.ts +10 -0
- package/src/loader/meta-data-loader.ts +31 -1
- package/src/loader/validation-passes.ts +161 -0
- package/src/naming.ts +6 -5
- package/src/persistence/db/db-constants.ts +40 -0
- package/src/persistence/db/db-provider.ts +3 -3
- package/src/persistence/db/db-schema.ts +20 -0
- package/src/persistence/source/meta-source.ts +64 -2
- package/src/persistence/source/source-constants.ts +40 -1
- package/src/persistence/source/source-schema.ts +78 -3
- package/src/persistence/source/validate-source-parameter-ref.ts +143 -0
- package/src/persistence/source/validate-source-physical-names.ts +123 -0
- package/src/serializer-json.ts +50 -0
- package/src/template/template-constants.ts +23 -0
- package/src/template/template-schema.ts +49 -2
- package/dist/constants.d.ts +0 -208
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -419
- package/dist/constants.js.map +0 -1
- package/dist/core/file-meta-data-loader.d.ts +0 -18
- package/dist/core/file-meta-data-loader.d.ts.map +0 -1
- package/dist/core/file-meta-data-loader.js +0 -81
- package/dist/core/file-meta-data-loader.js.map +0 -1
- package/dist/core/file-source.d.ts +0 -12
- package/dist/core/file-source.d.ts.map +0 -1
- package/dist/core/file-source.js +0 -46
- package/dist/core/file-source.js.map +0 -1
- package/dist/core-attr-schemas.d.ts +0 -22
- package/dist/core-attr-schemas.d.ts.map +0 -1
- package/dist/core-attr-schemas.js +0 -324
- package/dist/core-attr-schemas.js.map +0 -1
- package/dist/db/db-attr-schemas.d.ts +0 -8
- package/dist/db/db-attr-schemas.d.ts.map +0 -1
- package/dist/db/db-attr-schemas.js +0 -26
- package/dist/db/db-attr-schemas.js.map +0 -1
- package/dist/db/db-provider.d.ts +0 -3
- package/dist/db/db-provider.d.ts.map +0 -1
- package/dist/db/db-provider.js +0 -28
- package/dist/db/db-provider.js.map +0 -1
- package/dist/meta/find-reference.d.ts +0 -22
- package/dist/meta/find-reference.d.ts.map +0 -1
- package/dist/meta/find-reference.js +0 -29
- package/dist/meta/find-reference.js.map +0 -1
- package/dist/meta/meta-attr.d.ts +0 -8
- package/dist/meta/meta-attr.d.ts.map +0 -1
- package/dist/meta/meta-attr.js +0 -17
- package/dist/meta/meta-attr.js.map +0 -1
- package/dist/meta/meta-data.d.ts +0 -107
- package/dist/meta/meta-data.d.ts.map +0 -1
- package/dist/meta/meta-data.js +0 -302
- package/dist/meta/meta-data.js.map +0 -1
- package/dist/meta/meta-field.d.ts +0 -48
- package/dist/meta/meta-field.d.ts.map +0 -1
- package/dist/meta/meta-field.js +0 -94
- package/dist/meta/meta-field.js.map +0 -1
- package/dist/meta/meta-identity.d.ts +0 -71
- package/dist/meta/meta-identity.d.ts.map +0 -1
- package/dist/meta/meta-identity.js +0 -129
- package/dist/meta/meta-identity.js.map +0 -1
- package/dist/meta/meta-layout.d.ts +0 -23
- package/dist/meta/meta-layout.d.ts.map +0 -1
- package/dist/meta/meta-layout.js +0 -45
- package/dist/meta/meta-layout.js.map +0 -1
- package/dist/meta/meta-object.d.ts +0 -40
- package/dist/meta/meta-object.d.ts.map +0 -1
- package/dist/meta/meta-object.js +0 -81
- package/dist/meta/meta-object.js.map +0 -1
- package/dist/meta/meta-origin.d.ts +0 -32
- package/dist/meta/meta-origin.d.ts.map +0 -1
- package/dist/meta/meta-origin.js +0 -55
- package/dist/meta/meta-origin.js.map +0 -1
- package/dist/meta/meta-relationship.d.ts +0 -11
- package/dist/meta/meta-relationship.d.ts.map +0 -1
- package/dist/meta/meta-relationship.js +0 -27
- package/dist/meta/meta-relationship.js.map +0 -1
- package/dist/meta/meta-root.d.ts +0 -12
- package/dist/meta/meta-root.d.ts.map +0 -1
- package/dist/meta/meta-root.js +0 -24
- package/dist/meta/meta-root.js.map +0 -1
- package/dist/meta/meta-source.d.ts +0 -18
- package/dist/meta/meta-source.d.ts.map +0 -1
- package/dist/meta/meta-source.js +0 -31
- package/dist/meta/meta-source.js.map +0 -1
- package/dist/meta/meta-validator.d.ts +0 -29
- package/dist/meta/meta-validator.d.ts.map +0 -1
- package/dist/meta/meta-validator.js +0 -49
- package/dist/meta/meta-validator.js.map +0 -1
- package/dist/meta/meta-view.d.ts +0 -4
- package/dist/meta/meta-view.d.ts.map +0 -1
- package/dist/meta/meta-view.js +0 -8
- package/dist/meta/meta-view.js.map +0 -1
- package/dist/persistence/db/db-attr-schemas.d.ts +0 -8
- package/dist/persistence/db/db-attr-schemas.d.ts.map +0 -1
- package/dist/persistence/db/db-attr-schemas.js +0 -28
- package/dist/persistence/db/db-attr-schemas.js.map +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Validation pass: TPH discriminator cross-attribute rules (FR-014).
|
|
2
|
+
//
|
|
3
|
+
// Codes (all errors):
|
|
4
|
+
// ERR_DISCRIMINATOR_FIELD_NOT_FOUND — @discriminator names a field that
|
|
5
|
+
// does not exist on the entity (own
|
|
6
|
+
// or via extends chain).
|
|
7
|
+
// ERR_DISCRIMINATOR_VALUE_DUPLICATE — two subtypes of the same
|
|
8
|
+
// @discriminator-bearing root claim
|
|
9
|
+
// the same @discriminatorValue.
|
|
10
|
+
// ERR_DISCRIMINATOR_VALUE_MISSING — a concrete (non-abstract) entity
|
|
11
|
+
// extends a chain whose root carries
|
|
12
|
+
// @discriminator but the subtype lacks
|
|
13
|
+
// @discriminatorValue.
|
|
14
|
+
// ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH — @discriminatorValue cannot be
|
|
15
|
+
// coerced to the discriminator field's
|
|
16
|
+
// subtype (enum: not in @values;
|
|
17
|
+
// int: not numeric; string: always OK).
|
|
18
|
+
|
|
19
|
+
import type { MetaData } from "../../shared/meta-data.js";
|
|
20
|
+
import { ParseError } from "../../errors.js";
|
|
21
|
+
import { TYPE_OBJECT, TYPE_FIELD } from "../../shared/base-types.js";
|
|
22
|
+
import {
|
|
23
|
+
OBJECT_ATTR_DISCRIMINATOR,
|
|
24
|
+
OBJECT_ATTR_DISCRIMINATOR_VALUE,
|
|
25
|
+
OBJECT_SUBTYPE_ENTITY,
|
|
26
|
+
} from "./object-constants.js";
|
|
27
|
+
import {
|
|
28
|
+
FIELD_SUBTYPE_ENUM,
|
|
29
|
+
FIELD_SUBTYPE_INT,
|
|
30
|
+
FIELD_SUBTYPE_LONG,
|
|
31
|
+
FIELD_SUBTYPE_SHORT,
|
|
32
|
+
FIELD_SUBTYPE_BYTE,
|
|
33
|
+
FIELD_SUBTYPE_STRING,
|
|
34
|
+
FIELD_ATTR_VALUES,
|
|
35
|
+
} from "../field/field-constants.js";
|
|
36
|
+
|
|
37
|
+
const NUMERIC_DISCRIMINATOR_SUBTYPES = new Set<string>([
|
|
38
|
+
FIELD_SUBTYPE_INT,
|
|
39
|
+
FIELD_SUBTYPE_LONG,
|
|
40
|
+
FIELD_SUBTYPE_SHORT,
|
|
41
|
+
FIELD_SUBTYPE_BYTE,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export function validateDiscriminator(root: MetaData): ParseError[] {
|
|
45
|
+
const errors: ParseError[] = [];
|
|
46
|
+
|
|
47
|
+
const entities = root
|
|
48
|
+
.ownChildren()
|
|
49
|
+
.filter((c) => c.type === TYPE_OBJECT && c.subType === OBJECT_SUBTYPE_ENTITY);
|
|
50
|
+
|
|
51
|
+
// Pass 1: @discriminator on bases — name resolution (own + inherited fields).
|
|
52
|
+
for (const obj of entities) {
|
|
53
|
+
const disc = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
54
|
+
if (typeof disc !== "string" || disc === "") continue;
|
|
55
|
+
|
|
56
|
+
const field = findFieldOnEntity(obj, disc);
|
|
57
|
+
if (field === undefined) {
|
|
58
|
+
errors.push(
|
|
59
|
+
new ParseError(
|
|
60
|
+
`object.entity "${obj.name}" @discriminator: "${disc}" does not name a field on this entity ` +
|
|
61
|
+
`(checked own children and the extends chain)`,
|
|
62
|
+
{ code: "ERR_DISCRIMINATOR_FIELD_NOT_FOUND", source: obj.source },
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pass 2: @discriminatorValue validation — for every entity that declares it,
|
|
69
|
+
// walk up to find the @discriminator-bearing root, then check field membership
|
|
70
|
+
// and uniqueness within that root's subtype set.
|
|
71
|
+
type SubtypeBinding = {
|
|
72
|
+
subtype: MetaData;
|
|
73
|
+
value: string;
|
|
74
|
+
discField: MetaData;
|
|
75
|
+
};
|
|
76
|
+
const bindingsByRoot = new Map<MetaData, SubtypeBinding[]>();
|
|
77
|
+
|
|
78
|
+
for (const obj of entities) {
|
|
79
|
+
const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
|
|
80
|
+
if (typeof value !== "string" || value === "") continue;
|
|
81
|
+
|
|
82
|
+
const { root: discRoot, fieldName } = findDiscriminatorRoot(obj);
|
|
83
|
+
if (discRoot === undefined || fieldName === undefined) continue;
|
|
84
|
+
|
|
85
|
+
const field = findFieldOnEntity(discRoot, fieldName);
|
|
86
|
+
if (field === undefined) continue; // root's own ERR_DISCRIMINATOR_FIELD_NOT_FOUND already fires
|
|
87
|
+
|
|
88
|
+
// Type-match check.
|
|
89
|
+
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
90
|
+
const enumValues = field.ownAttr(FIELD_ATTR_VALUES);
|
|
91
|
+
const list = Array.isArray(enumValues) ? enumValues.map(String) : [];
|
|
92
|
+
if (!list.includes(value)) {
|
|
93
|
+
errors.push(
|
|
94
|
+
new ParseError(
|
|
95
|
+
`object.entity "${obj.name}" @discriminatorValue: "${value}" is not a member of the ` +
|
|
96
|
+
`discriminator enum field "${fieldName}" @values [${list.join(", ")}]`,
|
|
97
|
+
{ code: "ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH", source: obj.source },
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} else if (NUMERIC_DISCRIMINATOR_SUBTYPES.has(field.subType)) {
|
|
102
|
+
if (!/^-?\d+$/.test(value)) {
|
|
103
|
+
errors.push(
|
|
104
|
+
new ParseError(
|
|
105
|
+
`object.entity "${obj.name}" @discriminatorValue: "${value}" does not coerce to ` +
|
|
106
|
+
`numeric discriminator field "${fieldName}" (field.${field.subType})`,
|
|
107
|
+
{ code: "ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH", source: obj.source },
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} else if (field.subType !== FIELD_SUBTYPE_STRING) {
|
|
112
|
+
// Discriminator fields outside {enum, integer-family, string} are
|
|
113
|
+
// unusual; accept silently (a future refinement may add a warning).
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let list = bindingsByRoot.get(discRoot);
|
|
117
|
+
if (list === undefined) {
|
|
118
|
+
list = [];
|
|
119
|
+
bindingsByRoot.set(discRoot, list);
|
|
120
|
+
}
|
|
121
|
+
list.push({ subtype: obj, value, discField: field });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Pass 3: ERR_DISCRIMINATOR_VALUE_DUPLICATE within each root's subtypes.
|
|
125
|
+
for (const [, bindings] of bindingsByRoot) {
|
|
126
|
+
const seen = new Map<string, MetaData>();
|
|
127
|
+
for (const b of bindings) {
|
|
128
|
+
const prev = seen.get(b.value);
|
|
129
|
+
if (prev !== undefined) {
|
|
130
|
+
errors.push(
|
|
131
|
+
new ParseError(
|
|
132
|
+
`object.entity "${b.subtype.name}" @discriminatorValue: "${b.value}" ` +
|
|
133
|
+
`duplicates the value already claimed by "${prev.name}"`,
|
|
134
|
+
{ code: "ERR_DISCRIMINATOR_VALUE_DUPLICATE", source: b.subtype.source },
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
seen.set(b.value, b.subtype);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pass 4: ERR_DISCRIMINATOR_VALUE_MISSING — every concrete (non-abstract)
|
|
144
|
+
// entity that extends a @discriminator-bearing root must declare a value.
|
|
145
|
+
for (const obj of entities) {
|
|
146
|
+
if (obj.isAbstract === true) continue;
|
|
147
|
+
if (typeof obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) === "string") continue;
|
|
148
|
+
if (typeof obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR) === "string") continue; // a root, not a subtype
|
|
149
|
+
const { root: discRoot } = findDiscriminatorRoot(obj);
|
|
150
|
+
if (discRoot === undefined) continue;
|
|
151
|
+
if (discRoot === obj) continue; // safety: same node, shouldn't happen
|
|
152
|
+
errors.push(
|
|
153
|
+
new ParseError(
|
|
154
|
+
`object.entity "${obj.name}" extends the @discriminator-bearing root "${discRoot.name}" ` +
|
|
155
|
+
`but is missing @discriminatorValue (required on every concrete subtype)`,
|
|
156
|
+
{ code: "ERR_DISCRIMINATOR_VALUE_MISSING", source: obj.source },
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return errors;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Find a field with the given name on `entity` — own first, then via the
|
|
165
|
+
* resolved extends chain. Returns the declaring field node (own attrs intact). */
|
|
166
|
+
function findFieldOnEntity(entity: MetaData, name: string): MetaData | undefined {
|
|
167
|
+
for (const child of entity.ownChildren()) {
|
|
168
|
+
if (child.type === TYPE_FIELD && child.name === name) return child;
|
|
169
|
+
}
|
|
170
|
+
let cursor = entity.superResolved;
|
|
171
|
+
while (cursor !== undefined) {
|
|
172
|
+
for (const child of cursor.ownChildren()) {
|
|
173
|
+
if (child.type === TYPE_FIELD && child.name === name) return child;
|
|
174
|
+
}
|
|
175
|
+
cursor = cursor.superResolved;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Walk the extends chain to find the first ancestor (or self) carrying
|
|
181
|
+
* @discriminator. Returns {root, fieldName} when found; both undefined
|
|
182
|
+
* otherwise. */
|
|
183
|
+
function findDiscriminatorRoot(
|
|
184
|
+
entity: MetaData,
|
|
185
|
+
): { root: MetaData | undefined; fieldName: string | undefined } {
|
|
186
|
+
let cursor: MetaData | undefined = entity;
|
|
187
|
+
while (cursor !== undefined) {
|
|
188
|
+
const v = cursor.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
189
|
+
if (typeof v === "string" && v !== "") {
|
|
190
|
+
return { root: cursor, fieldName: v };
|
|
191
|
+
}
|
|
192
|
+
cursor = cursor.superResolved;
|
|
193
|
+
}
|
|
194
|
+
return { root: undefined, fieldName: undefined };
|
|
195
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ValueObject — the map-backed default backing object for `object.value`.
|
|
2
|
+
//
|
|
3
|
+
// Idiomatic TS port of Java's ValueObject: a string-keyed map that holds its
|
|
4
|
+
// MetaObject back-reference and supports both declared fields and arbitrary
|
|
5
|
+
// overflow keys. THE unbound default produced by MetaObject.newInstance() when
|
|
6
|
+
// no native type is registered for the object's FQN. Reflection-free.
|
|
7
|
+
|
|
8
|
+
import type { MetaObject } from "./meta-object.js";
|
|
9
|
+
import type { MetaObjectAware } from "./meta-object-aware.js";
|
|
10
|
+
|
|
11
|
+
export class ValueObject implements MetaObjectAware {
|
|
12
|
+
private readonly _values = new Map<string, unknown>();
|
|
13
|
+
private _metaData: MetaObject | undefined = undefined;
|
|
14
|
+
|
|
15
|
+
/** Construct over an optional MetaObject back-reference (set at newInstance). */
|
|
16
|
+
constructor(mo?: MetaObject) {
|
|
17
|
+
this._metaData = mo;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// MetaObjectAware
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
getMetaData(): MetaObject | undefined {
|
|
25
|
+
return this._metaData;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setMetaData(mo: MetaObject): void {
|
|
29
|
+
this._metaData = mo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Map-backed value access — declared fields AND arbitrary overflow keys.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Read a value by key. Returns undefined for an unset key. */
|
|
37
|
+
get(name: string): unknown {
|
|
38
|
+
return this._values.get(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Write a value by key. Any string key is accepted (overflow-friendly). */
|
|
42
|
+
set(name: string, value: unknown): void {
|
|
43
|
+
this._values.set(name, value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** True if the key has been set (including to undefined). */
|
|
47
|
+
has(name: string): boolean {
|
|
48
|
+
return this._values.has(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove a key. */
|
|
52
|
+
delete(name: string): boolean {
|
|
53
|
+
return this._values.delete(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Insertion-ordered keys currently held. */
|
|
57
|
+
keys(): string[] {
|
|
58
|
+
return [...this._values.keys()];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A plain-object snapshot of the backing map (insertion-ordered). */
|
|
62
|
+
toObject(): Record<string, unknown> {
|
|
63
|
+
return Object.fromEntries(this._values);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { FIELD_SUBTYPE_UUID } from "../field/field-constants.js";
|
|
2
|
+
|
|
1
3
|
// Query concern constants — filter operators, sort order values.
|
|
2
4
|
//
|
|
3
5
|
// NOTE: `query` is NOT a metamodel node type — it has no subtype, schema, or
|
|
@@ -48,6 +50,9 @@ export const OPS_BY_SUBTYPE: Readonly<Record<string, readonly FilterOp[]>> = {
|
|
|
48
50
|
date: [FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE, FILTER_OP_IN, FILTER_OP_IS_NULL],
|
|
49
51
|
time: [FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE, FILTER_OP_IN, FILTER_OP_IS_NULL],
|
|
50
52
|
timestamp: [FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_GT, FILTER_OP_GTE, FILTER_OP_LT, FILTER_OP_LTE, FILTER_OP_IN, FILTER_OP_IS_NULL],
|
|
53
|
+
// uuid: identity-comparison ops only — no `like` (not free-text) and no
|
|
54
|
+
// ordering (`gt`/`lt` are meaningless on a UUID).
|
|
55
|
+
[FIELD_SUBTYPE_UUID]: [FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_IN, FILTER_OP_IS_NULL],
|
|
51
56
|
} as const;
|
|
52
57
|
|
|
53
58
|
export function opsForSubType(subType: string): readonly FilterOp[] {
|
package/src/core-types.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { MetaRelationship } from "./core/relationship/meta-relationship.js";
|
|
|
35
35
|
import { MetaLayout } from "./presentation/layout/meta-layout.js";
|
|
36
36
|
import { MetaSource } from "./persistence/source/meta-source.js";
|
|
37
37
|
import { MetaOrigin, MetaPassthroughOrigin, MetaAggregateOrigin, MetaCollectionOrigin } from "./persistence/origin/meta-origin.js";
|
|
38
|
-
import { commonFieldAttrs, currencyFieldAttr, enumFieldAttr, enumAliasAttr, enumDocAttr } from "./core/field/field-schema.js";
|
|
38
|
+
import { commonFieldAttrs, currencyFieldAttr, enumFieldAttr, enumAliasAttr, enumDocAttr, coerceDefaultAttr, normalizeAttr } from "./core/field/field-schema.js";
|
|
39
39
|
import { objectAttrs } from "./core/object/object-schema.js";
|
|
40
40
|
import { relationshipAttrs } from "./core/relationship/relationship-schema.js";
|
|
41
41
|
import { identityFieldsAttr, IDENTITY_ATTRS_MAP } from "./core/identity/identity-schema.js";
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
SUBTYPE_ROOT,
|
|
63
63
|
} from "./shared/base-types.js";
|
|
64
64
|
import { CHILD_RULE_WILDCARD } from "./shared/structural.js";
|
|
65
|
-
import { OBJECT_SUBTYPES, OBJECT_SUBTYPE_ENTITY } from "./core/object/object-constants.js";
|
|
65
|
+
import { OBJECT_SUBTYPES, OBJECT_SUBTYPE_ENTITY, OBJECT_SUBTYPE_VALUE } from "./core/object/object-constants.js";
|
|
66
66
|
import { FIELD_SUBTYPES, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_ENUM } from "./core/field/field-constants.js";
|
|
67
67
|
import { ATTR_SUBTYPES } from "./core/attr/attr-constants.js";
|
|
68
68
|
import {
|
|
@@ -183,8 +183,14 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
|
|
|
183
183
|
wildcard(TYPE_ATTR),
|
|
184
184
|
];
|
|
185
185
|
for (const subType of OBJECT_SUBTYPES) {
|
|
186
|
+
// FR-011: object.value additionally carries @normalize — the object-level
|
|
187
|
+
// default normalization mode for its enum fields' tolerant extract.
|
|
188
|
+
const subTypeObjectAttrs =
|
|
189
|
+
subType === OBJECT_SUBTYPE_VALUE
|
|
190
|
+
? [...objectAttrs, { ...normalizeAttr }]
|
|
191
|
+
: [...objectAttrs];
|
|
186
192
|
registry.register(
|
|
187
|
-
def(TYPE_OBJECT, subType, `Object/entity (${subType})`, objectRules, MetaObject,
|
|
193
|
+
def(TYPE_OBJECT, subType, `Object/entity (${subType})`, objectRules, MetaObject, subTypeObjectAttrs),
|
|
188
194
|
);
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -202,7 +208,14 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
|
|
|
202
208
|
subType === FIELD_SUBTYPE_CURRENCY
|
|
203
209
|
? [...commonFieldAttrs, { ...currencyFieldAttr }]
|
|
204
210
|
: subType === FIELD_SUBTYPE_ENUM
|
|
205
|
-
? [
|
|
211
|
+
? [
|
|
212
|
+
...commonFieldAttrs,
|
|
213
|
+
{ ...enumFieldAttr },
|
|
214
|
+
{ ...enumAliasAttr },
|
|
215
|
+
{ ...enumDocAttr },
|
|
216
|
+
{ ...coerceDefaultAttr },
|
|
217
|
+
{ ...normalizeAttr },
|
|
218
|
+
]
|
|
206
219
|
: [...commonFieldAttrs];
|
|
207
220
|
registry.register(
|
|
208
221
|
def(TYPE_FIELD, subType, `Field of type ${subType}`, fieldRules, MetaField, fieldAttrs,
|
package/src/errors.ts
CHANGED
|
@@ -49,6 +49,22 @@ export const ERROR_CODES = [
|
|
|
49
49
|
"ERR_RESERVED_ATTR",
|
|
50
50
|
"ERR_SOURCE_NO_PRIMARY",
|
|
51
51
|
"ERR_SOURCE_MULTIPLE_PRIMARY",
|
|
52
|
+
// FR-016 / ADR-0018 — per-kind physical-name alias validation on source.rdb.
|
|
53
|
+
"ERR_PHYSICAL_NAME_KIND_MISMATCH",
|
|
54
|
+
"ERR_PHYSICAL_NAME_MULTIPLE",
|
|
55
|
+
// FR-013 — field-level @readOnly cross-attribute validation.
|
|
56
|
+
"ERR_READONLY_ASSIGNED_PRIMARY",
|
|
57
|
+
"ERR_READONLY_DOWNGRADE",
|
|
58
|
+
// FR-015 — source.rdb @parameterRef typed-input validation.
|
|
59
|
+
"ERR_PARAMETER_REF_UNRESOLVED",
|
|
60
|
+
"ERR_PARAMETER_REF_NOT_VALUE_OBJECT",
|
|
61
|
+
"ERR_PARAMETER_REF_ON_NON_CALLABLE_KIND",
|
|
62
|
+
"ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH",
|
|
63
|
+
// FR-014 — TPH discriminator cross-attribute validation.
|
|
64
|
+
"ERR_DISCRIMINATOR_FIELD_NOT_FOUND",
|
|
65
|
+
"ERR_DISCRIMINATOR_VALUE_DUPLICATE",
|
|
66
|
+
"ERR_DISCRIMINATOR_VALUE_MISSING",
|
|
67
|
+
"ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH",
|
|
52
68
|
// ADR-0006 D2 — YAML type-coercion guard. Emitted by every port's YAML
|
|
53
69
|
// loader when a coerced scalar mismatches the schema-declared type.
|
|
54
70
|
"ERR_YAML_COERCION",
|
|
@@ -67,6 +83,13 @@ export const WARNING_CODES = [
|
|
|
67
83
|
// strings; wrapped at the loader boundary into the envelope shape with
|
|
68
84
|
// this code. Retired as those sites are migrated to envelopes.
|
|
69
85
|
"WARN_LEGACY",
|
|
86
|
+
// FR-016 / ADR-0018 — pre-1.0 legacy @table spelling on a non-table @kind.
|
|
87
|
+
// Loader accepts; canonical-serializer rewrites to the kind-matching alias.
|
|
88
|
+
"WARN_LEGACY_PHYSICAL_NAME_ALIAS",
|
|
89
|
+
// FR-013 — @readOnly on a field child of object.value. The persistence
|
|
90
|
+
// implication does not apply to value-objects; the attr is retained for
|
|
91
|
+
// language-specific record/struct treatment (e.g. Kotlin `val` vs `var`).
|
|
92
|
+
"WARN_READONLY_VALUE_OBJECT",
|
|
70
93
|
] as const;
|
|
71
94
|
export type WarningCode = (typeof WARNING_CODES)[number];
|
|
72
95
|
|
package/src/index.ts
CHANGED
|
@@ -50,6 +50,16 @@ export { MetaRoot } from "./shared/meta-root.js";
|
|
|
50
50
|
export { MetaObject } from "./core/object/meta-object.js";
|
|
51
51
|
export { MetaField } from "./core/field/meta-field.js";
|
|
52
52
|
export { MetaAttr } from "./core/attr/meta-attr.js";
|
|
53
|
+
|
|
54
|
+
// Runtime object model — backing objects + FQN→factory binding (Phase A).
|
|
55
|
+
export { ValueObject } from "./core/object/value-object.js";
|
|
56
|
+
export { isMetaObjectAware } from "./core/object/meta-object-aware.js";
|
|
57
|
+
export type { MetaObjectAware } from "./core/object/meta-object-aware.js";
|
|
58
|
+
export {
|
|
59
|
+
ObjectClassRegistry,
|
|
60
|
+
defaultObjectClassRegistry,
|
|
61
|
+
} from "./core/object/object-class-registry.js";
|
|
62
|
+
export type { ObjectFactory } from "./core/object/object-class-registry.js";
|
|
53
63
|
// Identity: base + subtype-specific
|
|
54
64
|
export {
|
|
55
65
|
MetaIdentity,
|
|
@@ -17,8 +17,12 @@ import { ParseError } from "../errors.js";
|
|
|
17
17
|
import type { LoaderWarning } from "../source.js";
|
|
18
18
|
import { codeSource, resolvedSource } from "../source.js";
|
|
19
19
|
import { parseJson } from "../parser-json.js";
|
|
20
|
-
import { validateDataGridSortFields, validateFilterableHasIndex, validateOriginPaths, validateDataGridFilterValues, validateFieldObjectStorage, validateTemplatePayloadRefs } from "./validation-passes.js";
|
|
20
|
+
import { validateDataGridSortFields, validateFilterableHasIndex, validateOriginPaths, validateDataGridFilterValues, validateFieldObjectStorage, validateTemplatePayloadRefs, validateFieldDefaults } from "./validation-passes.js";
|
|
21
21
|
import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
|
|
22
|
+
import { validateSourcePhysicalNames } from "../persistence/source/validate-source-physical-names.js";
|
|
23
|
+
import { validateSourceParameterRef } from "../persistence/source/validate-source-parameter-ref.js";
|
|
24
|
+
import { validateFieldReadOnly } from "../core/field/validate-field-readonly.js";
|
|
25
|
+
import { validateDiscriminator } from "../core/object/validate-discriminator.js";
|
|
22
26
|
import { resolveDeferredSupers } from "../super-resolve.js";
|
|
23
27
|
import { validateSubtypeRules } from "../subtype-rules.js";
|
|
24
28
|
import { validateAttrSchema } from "../attr-schema-validate.js";
|
|
@@ -441,6 +445,32 @@ export class MetaDataLoader {
|
|
|
441
445
|
// exactly one must carry role "primary" (ERR_SOURCE_NO_PRIMARY /
|
|
442
446
|
// ERR_SOURCE_MULTIPLE_PRIMARY).
|
|
443
447
|
errors.push(...validateSourceRoles(root));
|
|
448
|
+
|
|
449
|
+
// FR-016 / ADR-0018 — per-kind physical-name alias validation on
|
|
450
|
+
// source.rdb (kind-matching alias, no multiples, legacy @table warning).
|
|
451
|
+
const physicalNameResult = validateSourcePhysicalNames(root);
|
|
452
|
+
errors.push(...physicalNameResult.errors);
|
|
453
|
+
envelopeWarnings.push(...physicalNameResult.warnings);
|
|
454
|
+
|
|
455
|
+
// FR-013 — field-level @readOnly cross-attribute rules
|
|
456
|
+
// (ERR_READONLY_DOWNGRADE / ERR_READONLY_ASSIGNED_PRIMARY /
|
|
457
|
+
// WARN_READONLY_VALUE_OBJECT).
|
|
458
|
+
const readOnlyResult = validateFieldReadOnly(root);
|
|
459
|
+
errors.push(...readOnlyResult.errors);
|
|
460
|
+
envelopeWarnings.push(...readOnlyResult.warnings);
|
|
461
|
+
|
|
462
|
+
// FR-015 — source.rdb @parameterRef typed-input validation.
|
|
463
|
+
errors.push(...validateSourceParameterRef(root));
|
|
464
|
+
|
|
465
|
+
// FR-014 — TPH discriminator (@discriminator / @discriminatorValue)
|
|
466
|
+
// cross-attribute validation.
|
|
467
|
+
errors.push(...validateDiscriminator(root));
|
|
468
|
+
|
|
469
|
+
// Eleventh pass: per-type @default coercibility — a field's @default value
|
|
470
|
+
// must coerce to the field's type (int/long → integer, double/float/decimal →
|
|
471
|
+
// finite number, boolean → true|false). Enum @default membership is validated
|
|
472
|
+
// by validateAttrSchema (Check 5). Cross-port parity with Java/Python/C#.
|
|
473
|
+
errors.push(...validateFieldDefaults(root));
|
|
444
474
|
}
|
|
445
475
|
|
|
446
476
|
// If nothing parsed successfully, synthesize an empty root so callers
|
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
26
26
|
TEMPLATE_ATTR_REQUIRED_SLOTS,
|
|
27
|
+
TEMPLATE_ATTR_TEXT_REF,
|
|
28
|
+
TEMPLATE_ATTR_KIND,
|
|
29
|
+
TEMPLATE_KIND_EMAIL,
|
|
30
|
+
TEMPLATE_ATTR_SUBJECT_REF,
|
|
31
|
+
TEMPLATE_ATTR_HTML_BODY_REF,
|
|
32
|
+
TEMPLATE_SUBTYPE_OUTPUT,
|
|
33
|
+
TEMPLATE_SUBTYPE_PROMPT,
|
|
27
34
|
} from "../template/template-constants.js";
|
|
28
35
|
import { OBJECT_SUBTYPE_VALUE } from "../core/object/object-constants.js";
|
|
29
36
|
import {
|
|
@@ -36,6 +43,15 @@ import {
|
|
|
36
43
|
FIELD_ATTR_OBJECT_REF,
|
|
37
44
|
FIELD_ATTR_STORAGE,
|
|
38
45
|
STORAGE_FLATTENED,
|
|
46
|
+
FIELD_ATTR_DEFAULT,
|
|
47
|
+
FIELD_SUBTYPE_INT,
|
|
48
|
+
FIELD_SUBTYPE_LONG,
|
|
49
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
50
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
51
|
+
FIELD_SUBTYPE_FLOAT,
|
|
52
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
53
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
54
|
+
FIELD_SUBTYPE_ENUM,
|
|
39
55
|
} from "../core/field/field-constants.js";
|
|
40
56
|
import { FIELD_ATTR_DB_INDEXED } from "../persistence/db/db-constants.js";
|
|
41
57
|
import { IDENTITY_ATTR_FIELDS } from "../core/identity/identity-constants.js";
|
|
@@ -97,6 +113,56 @@ export function validateDataGridSortFields(root: MetaData): ParseError[] {
|
|
|
97
113
|
export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
98
114
|
const errors: ParseError[] = [];
|
|
99
115
|
for (const tmpl of root.ownChildren().filter((c) => c.type === TYPE_TEMPLATE)) {
|
|
116
|
+
// --- @kind / textRef / email part-ref cross-field rules ---
|
|
117
|
+
// template.output is either a document (@kind absent/"document" → @textRef
|
|
118
|
+
// required) or an email (@kind="email" → @subjectRef + @htmlBodyRef required,
|
|
119
|
+
// @textRef unused). template.prompt always requires @textRef (the renderable
|
|
120
|
+
// body). The closed-enum membership of @kind is handled by validateAttrSchema
|
|
121
|
+
// (allowedValues) — here we only enforce the conditional ref presence.
|
|
122
|
+
if (tmpl.subType === TEMPLATE_SUBTYPE_OUTPUT) {
|
|
123
|
+
const kind = tmpl.ownAttr(TEMPLATE_ATTR_KIND);
|
|
124
|
+
if (kind === TEMPLATE_KIND_EMAIL) {
|
|
125
|
+
if (typeof tmpl.ownAttr(TEMPLATE_ATTR_SUBJECT_REF) !== "string") {
|
|
126
|
+
errors.push(
|
|
127
|
+
new ParseError(
|
|
128
|
+
`template "${tmpl.name}" @kind "email" requires @subjectRef`,
|
|
129
|
+
{ code: "ERR_INVALID_TEMPLATE", source: tmpl.source },
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (typeof tmpl.ownAttr(TEMPLATE_ATTR_HTML_BODY_REF) !== "string") {
|
|
134
|
+
errors.push(
|
|
135
|
+
new ParseError(
|
|
136
|
+
`template "${tmpl.name}" @kind "email" requires @htmlBodyRef`,
|
|
137
|
+
{ code: "ERR_INVALID_TEMPLATE", source: tmpl.source },
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// @kind absent or "document" → require @textRef. (An out-of-enum @kind is
|
|
143
|
+
// separately reported by validateAttrSchema; we still require @textRef so a
|
|
144
|
+
// document is never bodyless.)
|
|
145
|
+
if (typeof tmpl.ownAttr(TEMPLATE_ATTR_TEXT_REF) !== "string") {
|
|
146
|
+
errors.push(
|
|
147
|
+
new ParseError(
|
|
148
|
+
`template "${tmpl.name}" @kind "document" requires @textRef`,
|
|
149
|
+
{ code: "ERR_INVALID_TEMPLATE", source: tmpl.source },
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (tmpl.subType === TEMPLATE_SUBTYPE_PROMPT) {
|
|
155
|
+
// template.prompt always carries a renderable body via @textRef.
|
|
156
|
+
if (typeof tmpl.ownAttr(TEMPLATE_ATTR_TEXT_REF) !== "string") {
|
|
157
|
+
errors.push(
|
|
158
|
+
new ParseError(
|
|
159
|
+
`template "${tmpl.name}" requires @textRef`,
|
|
160
|
+
{ code: "ERR_INVALID_TEMPLATE", source: tmpl.source },
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
100
166
|
const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
101
167
|
if (typeof payloadRef !== "string") continue; // absence handled by the required-attr schema check
|
|
102
168
|
const payload = root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === payloadRef);
|
|
@@ -454,6 +520,101 @@ export function validateFieldObjectStorage(root: MetaData): ParseError[] {
|
|
|
454
520
|
return errors;
|
|
455
521
|
}
|
|
456
522
|
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// Per-type @default coercibility validation (Phase B — generalized @default)
|
|
525
|
+
//
|
|
526
|
+
// The @default attr is registered on the field base, so any field subtype may
|
|
527
|
+
// declare it. Its value must coerce to the field's type (cross-port parity with
|
|
528
|
+
// Java ValidationPhase.validateFieldDefaults, Python _validate_field_defaults,
|
|
529
|
+
// C# ValidateFieldDefaults):
|
|
530
|
+
// - int / long / currency → ASCII integer parse (or finite decimal that
|
|
531
|
+
// truncates — matches the engine's Coerce INT/LONG fallback)
|
|
532
|
+
// - double / float / decimal → finite-number parse (ASCII)
|
|
533
|
+
// - boolean → exactly "true" | "false"
|
|
534
|
+
// - enum → member of @values (handled by attr-schema-validate
|
|
535
|
+
// Check 5; SKIPPED here to avoid double-emit)
|
|
536
|
+
// - string / date / time / object / others → any value allowed
|
|
537
|
+
// A violation emits ERR_BAD_ATTR_VALUE on the field node's source.
|
|
538
|
+
//
|
|
539
|
+
// Own-only: validates @default declared on THIS node (ownAttr), matching the
|
|
540
|
+
// @values / FR-011 own-attr passes. Numeric gates are ASCII-only — they reject
|
|
541
|
+
// "1_000" separators, "0x.."/radix literals, and unicode digits (JS Number()
|
|
542
|
+
// would accept some of these). This mirrors Java's Long.parseLong / Double.parseDouble
|
|
543
|
+
// strictness exactly.
|
|
544
|
+
//
|
|
545
|
+
// The @default value is type-preserved by the parser (a JSON true/false → boolean,
|
|
546
|
+
// a JSON number → number, a JSON string → string), so it is stringified to the
|
|
547
|
+
// canonical form before the per-type gate (lower-case bool, plain number).
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
const _INT_DEFAULT_SUBTYPES = new Set<string>([
|
|
551
|
+
FIELD_SUBTYPE_INT,
|
|
552
|
+
FIELD_SUBTYPE_LONG,
|
|
553
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
554
|
+
]);
|
|
555
|
+
const _NUM_DEFAULT_SUBTYPES = new Set<string>([
|
|
556
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
557
|
+
FIELD_SUBTYPE_FLOAT,
|
|
558
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
559
|
+
]);
|
|
560
|
+
|
|
561
|
+
// ASCII-only integer: optional sign, then digits. No separators, radix, unicode.
|
|
562
|
+
const _ASCII_INT = /^[+-]?\d+$/;
|
|
563
|
+
// ASCII-only decimal: optional sign, digits with optional fraction/exponent.
|
|
564
|
+
// No separators, no hex, no Infinity/NaN.
|
|
565
|
+
const _ASCII_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
566
|
+
|
|
567
|
+
/** Canonical string form of a type-preserved @default value (lower-case bool, plain number). */
|
|
568
|
+
function _stringifyDefault(value: string | number | boolean): string {
|
|
569
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
570
|
+
return String(value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** ASCII integer, or a finite ASCII decimal that truncates to an integer (Java parsesAsLong parity). */
|
|
574
|
+
function _parsesAsLong(s: string): boolean {
|
|
575
|
+
const t = s.trim();
|
|
576
|
+
if (_ASCII_INT.test(t)) return true;
|
|
577
|
+
// accept a finite decimal that truncates to an integer value
|
|
578
|
+
return _ASCII_NUMBER.test(t) && Number.isFinite(Number(t));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Finite ASCII number (Java parsesAsFiniteNumber parity). */
|
|
582
|
+
function _parsesAsFiniteNumber(s: string): boolean {
|
|
583
|
+
const t = s.trim();
|
|
584
|
+
return _ASCII_NUMBER.test(t) && Number.isFinite(Number(t));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function validateFieldDefaults(root: MetaData): ParseError[] {
|
|
588
|
+
const errors: ParseError[] = [];
|
|
589
|
+
_walkFieldDefaults(root, errors);
|
|
590
|
+
return errors;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function _walkFieldDefaults(node: MetaData, errors: ParseError[]): void {
|
|
594
|
+
if (node.type === TYPE_FIELD && node.subType !== FIELD_SUBTYPE_ENUM) {
|
|
595
|
+
// Enum @default membership is validated by attr-schema-validate Check 5.
|
|
596
|
+
const raw = node.ownAttr(FIELD_ATTR_DEFAULT);
|
|
597
|
+
if (raw !== undefined && raw !== null && !Array.isArray(raw) && typeof raw !== "object") {
|
|
598
|
+
const def = _stringifyDefault(raw as string | number | boolean);
|
|
599
|
+
const sub = node.subType;
|
|
600
|
+
let ok: boolean;
|
|
601
|
+
if (_INT_DEFAULT_SUBTYPES.has(sub)) ok = _parsesAsLong(def);
|
|
602
|
+
else if (_NUM_DEFAULT_SUBTYPES.has(sub)) ok = _parsesAsFiniteNumber(def);
|
|
603
|
+
else if (sub === FIELD_SUBTYPE_BOOLEAN) ok = def === "true" || def === "false";
|
|
604
|
+
else ok = true; // string / date / time / object / others — any value allowed
|
|
605
|
+
if (!ok) {
|
|
606
|
+
errors.push(
|
|
607
|
+
new ParseError(
|
|
608
|
+
`field.${sub} "${node.name}" @${FIELD_ATTR_DEFAULT} "${def}" is not coercible to the field's type`,
|
|
609
|
+
{ code: "ERR_BAD_ATTR_VALUE", source: node.source },
|
|
610
|
+
),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const child of node.ownChildren()) _walkFieldDefaults(child, errors);
|
|
616
|
+
}
|
|
617
|
+
|
|
457
618
|
// ---------------------------------------------------------------------------
|
|
458
619
|
// Layout dataGrid @filter value validation
|
|
459
620
|
//
|
package/src/naming.ts
CHANGED
|
@@ -60,14 +60,15 @@ export function pluralize(s: string): string {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function resolveTableName(entity: MetaData): string {
|
|
63
|
-
//
|
|
64
|
-
// (table
|
|
65
|
-
//
|
|
63
|
+
// FR-016: primary source's `physicalName` implements the four-step rule
|
|
64
|
+
// (kind-matching alias → legacy @table → source.name → entity-name fallback),
|
|
65
|
+
// so this helper now just delegates. Writability (table vs view/storedProc/
|
|
66
|
+
// tableFunction) only affects write-routing — for SELECT-side name resolution,
|
|
67
|
+
// a read-only primary source is the right answer.
|
|
66
68
|
const source = entity.ownChildren().find(
|
|
67
69
|
(c): c is MetaSource => c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
|
|
68
70
|
);
|
|
69
|
-
|
|
70
|
-
if (typeof name === "string" && name !== "") return name;
|
|
71
|
+
if (source !== undefined) return source.physicalName;
|
|
71
72
|
return pluralize(toSnakeCase(entity.name));
|
|
72
73
|
}
|
|
73
74
|
|