@metaobjectsdev/metadata 0.24.0 → 0.24.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/core/identity/identity-definition.embedded.js +2 -2
- package/dist/core/identity/identity-definition.embedded.js.map +1 -1
- package/dist/core/index/index-definition.embedded.js +2 -2
- package/dist/core/index/index-definition.embedded.js.map +1 -1
- package/dist/core/vocabulary-rewrite-yaml.d.ts +21 -0
- package/dist/core/vocabulary-rewrite-yaml.d.ts.map +1 -0
- package/dist/core/vocabulary-rewrite-yaml.js +245 -0
- package/dist/core/vocabulary-rewrite-yaml.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -2
- package/dist/errors.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +4 -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 +261 -43
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +5 -5
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/registry-manifest.d.ts +1 -1
- package/dist/registry-manifest.js +1 -1
- package/dist/vocabulary-rewrite.d.ts.map +1 -1
- package/dist/vocabulary-rewrite.js +12 -8
- package/dist/vocabulary-rewrite.js.map +1 -1
- package/package.json +6 -1
- package/src/core/identity/identity-definition.embedded.ts +2 -2
- package/src/core/index/index-definition.embedded.ts +2 -2
- package/src/core/vocabulary-rewrite-yaml.ts +267 -0
- package/src/errors.ts +21 -2
- package/src/loader/meta-data-loader.ts +5 -1
- package/src/loader/validation-passes.ts +320 -47
- package/src/persistence/origin/origin-definition.embedded.ts +5 -5
- package/src/registry-manifest.ts +1 -1
- package/src/vocabulary-rewrite.ts +12 -8
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/core/vocabulary-rewrite-yaml.ts
|
|
2
|
+
//
|
|
3
|
+
// The YAML arm of the raw-document rewriter behind `meta upgrade`.
|
|
4
|
+
//
|
|
5
|
+
// WHY IT IS A SEPARATE MODULE. `vocabulary-rewrite.ts` is reachable from `src/index.ts`, so
|
|
6
|
+
// it may not import `yaml` — that package is Node-only and would land in the browser bundle.
|
|
7
|
+
// This file carries the `yaml` dependency and is reachable ONLY through its own package
|
|
8
|
+
// subpath, which `meta upgrade` dynamic-imports. Same split, and same reason, as
|
|
9
|
+
// `yaml-positions.ts` / `yaml-positions-walker.ts` (see that file's header).
|
|
10
|
+
//
|
|
11
|
+
// WHY IT IS PARSER-DRIVEN WHERE THE JSON ARM IS REGEX-DRIVEN. A hand-rolled YAML mode was
|
|
12
|
+
// tried once and shipped a file-corrupting bug: a multi-item block sequence lost every item
|
|
13
|
+
// but the first, because a hand-written value scanner stops at a newline, and the dominant
|
|
14
|
+
// in-repo flow style (`{ name: x, readOnly: true }`) was not matched at all — so the rename
|
|
15
|
+
// silently did nothing. Both failures are the same failure: YAML's value extent is not
|
|
16
|
+
// derivable by scanning. Here the PARSER reports it. `pair.value.range` covers a four-line
|
|
17
|
+
// block sequence and a one-line flow mapping alike, so neither case is a special case.
|
|
18
|
+
//
|
|
19
|
+
// STILL SURGICAL, NOT PARSE-AND-REPRINT. `doc.toString()` would reflow an adopter's file —
|
|
20
|
+
// line width, quote style, indentation of flow collections — and hand them a diff whose real
|
|
21
|
+
// changes are invisible inside it. So the parse is used only to LOCATE spans; every edit is a
|
|
22
|
+
// span replacement on the original text, and any region not deliberately changed comes back
|
|
23
|
+
// byte-identical. That is the same guarantee the JSON arm makes, by the same means.
|
|
24
|
+
//
|
|
25
|
+
// SIGIL-FREE, PER ADR-0006. YAML authoring writes bare attribute keys (`violation:`) and the
|
|
26
|
+
// desugar re-adds the `@` when lowering to canonical JSON. So a rename emits a BARE key here
|
|
27
|
+
// where the JSON arm emits `"@name"`. A leading `@` is still matched on input — an author who
|
|
28
|
+
// wrote one gets it fixed rather than skipped — but is never introduced.
|
|
29
|
+
|
|
30
|
+
import { LineCounter, isMap, isSeq, parseDocument, type Node, type Pair } from "yaml";
|
|
31
|
+
import {
|
|
32
|
+
RETIRED_VOCABULARY,
|
|
33
|
+
note,
|
|
34
|
+
scopeMatches,
|
|
35
|
+
type RetiredEntry,
|
|
36
|
+
} from "../retired-vocabulary.js";
|
|
37
|
+
import type { RewriteChange, RewriteRefusal, RewriteOpts, RewriteResult } from "../vocabulary-rewrite.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A rewrite result that can also report "I could not read this file".
|
|
41
|
+
*
|
|
42
|
+
* A YAML document that does not parse yields no changes and no refusals, which is
|
|
43
|
+
* indistinguishable from a clean one — and a fixer that reports a file it could not open as
|
|
44
|
+
* clean is the exact defect this arm was written to remove (#339). The flag makes the caller
|
|
45
|
+
* say so out loud.
|
|
46
|
+
*/
|
|
47
|
+
export interface YamlRewriteResult extends RewriteResult {
|
|
48
|
+
readonly unparseable: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A canonical node key: `<type>.<subType>`. Identical to the JSON arm's scope shape. */
|
|
52
|
+
const TYPE_KEY = /^[a-z][A-Za-z0-9]*\.[A-Za-z0-9_*]+$/;
|
|
53
|
+
|
|
54
|
+
/** `0.24.0` → `[0,24,0]`, for an ordered comparison rather than a string one. */
|
|
55
|
+
function parts(v: string): number[] {
|
|
56
|
+
return v.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function atOrBefore(a: string, b: string): boolean {
|
|
60
|
+
const [x, y] = [parts(a), parts(b)];
|
|
61
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
62
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
63
|
+
if (d !== 0) return d < 0;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The plain string a mapping key carries, or undefined when it is not a plain scalar. */
|
|
69
|
+
function keyText(key: unknown): string | undefined {
|
|
70
|
+
const v = (key as { value?: unknown } | null)?.value;
|
|
71
|
+
return typeof v === "string" ? v : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Source offsets of a pair's key and of the end of its value. */
|
|
75
|
+
function pairSpan(pair: Pair): { keyStart: number; keyEnd: number; valueEnd: number } | undefined {
|
|
76
|
+
const k = pair.key as { range?: [number, number, number] } | null;
|
|
77
|
+
const v = pair.value as { range?: [number, number, number] } | null;
|
|
78
|
+
if (k?.range === undefined) return undefined;
|
|
79
|
+
return {
|
|
80
|
+
keyStart: k.range[0],
|
|
81
|
+
keyEnd: k.range[1],
|
|
82
|
+
// A valueless key (`verifiedBy:` with nothing after it) still has to be removable.
|
|
83
|
+
valueEnd: v?.range?.[1] ?? k.range[1],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The span to delete so that removing a pair leaves loadable YAML.
|
|
89
|
+
*
|
|
90
|
+
* Two shapes, and they need opposite treatment — which is precisely what the previous
|
|
91
|
+
* hand-rolled attempt got wrong by handling only one.
|
|
92
|
+
*/
|
|
93
|
+
function dropSpan(
|
|
94
|
+
source: string,
|
|
95
|
+
span: { keyStart: number; valueEnd: number },
|
|
96
|
+
flow: boolean,
|
|
97
|
+
): { start: number; end: number } | undefined {
|
|
98
|
+
let { keyStart: start } = span;
|
|
99
|
+
let end = span.valueEnd;
|
|
100
|
+
|
|
101
|
+
if (flow) {
|
|
102
|
+
// `{ a: 1, readOnly: true }` — take a trailing comma if there is one, else a preceding
|
|
103
|
+
// one, so the mapping ends up with neither a dangling nor a doubled separator.
|
|
104
|
+
//
|
|
105
|
+
// The probe must not commit: scanning forward over the spaces and THEN finding `}`
|
|
106
|
+
// rather than `,` would leave `end` past the space that separates the survivor from the
|
|
107
|
+
// brace, silently reformatting `{ name: x }` into `{ name: x}`.
|
|
108
|
+
let probe = end;
|
|
109
|
+
while (probe < source.length && /[ \t]/.test(source[probe] ?? "")) probe++;
|
|
110
|
+
if (source[probe] === ",") {
|
|
111
|
+
end = probe + 1;
|
|
112
|
+
while (end < source.length && /[ \t]/.test(source[end] ?? "")) end++;
|
|
113
|
+
} else {
|
|
114
|
+
let back = start;
|
|
115
|
+
while (back > 0 && /\s/.test(source[back - 1] ?? "")) back--;
|
|
116
|
+
if (source[back - 1] === ",") start = back - 1;
|
|
117
|
+
}
|
|
118
|
+
return { start, end };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Block mapping — the pair owns whole lines. Absorb its indentation and its line
|
|
122
|
+
// terminator, so removal leaves neither a ragged line nor a blank one.
|
|
123
|
+
while (start > 0 && /[ \t]/.test(source[start - 1] ?? "")) start--;
|
|
124
|
+
|
|
125
|
+
// A pair that is the first key of a block SEQUENCE item (`- verifiedBy: x`) shares its
|
|
126
|
+
// line with the `-`. Deleting it would strand the dash and silently change the sequence's
|
|
127
|
+
// shape, so this refuses rather than guesses — the caller reports it as needing a hand.
|
|
128
|
+
if (source[start - 1] === "-") return undefined;
|
|
129
|
+
|
|
130
|
+
// A multi-line value (a block sequence) already ends ON the newline that closes its last
|
|
131
|
+
// item, so the terminator is spent. Consuming another one here would delete the FOLLOWING
|
|
132
|
+
// key — which is the multi-item-sequence corruption this arm exists to avoid, arriving by
|
|
133
|
+
// a different route.
|
|
134
|
+
if (end === 0 || source[end - 1] !== "\n") {
|
|
135
|
+
while (end < source.length && /[ \t]/.test(source[end] ?? "")) end++;
|
|
136
|
+
// A trailing comment on the key's own line goes with the key it annotates.
|
|
137
|
+
if (source[end] === "#") while (end < source.length && source[end] !== "\n") end++;
|
|
138
|
+
if (source[end] === "\n") end++;
|
|
139
|
+
}
|
|
140
|
+
return { start, end };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Visit every mapping pair with the `<type>.<subType>` scope governing it. */
|
|
144
|
+
function eachPair(
|
|
145
|
+
node: unknown,
|
|
146
|
+
scope: string | undefined,
|
|
147
|
+
visit: (pair: Pair, scope: string | undefined, flow: boolean) => void,
|
|
148
|
+
): void {
|
|
149
|
+
if (isMap(node)) {
|
|
150
|
+
const flow = node.flow === true;
|
|
151
|
+
for (const pair of node.items) {
|
|
152
|
+
const k = keyText(pair.key);
|
|
153
|
+
visit(pair, scope, flow);
|
|
154
|
+
// A type key scopes its own BODY, not itself — so the pair above is reported under the
|
|
155
|
+
// enclosing scope while its value descends under this one.
|
|
156
|
+
const inner = k !== undefined && TYPE_KEY.test(k) ? k : scope;
|
|
157
|
+
if (pair.value != null) eachPair(pair.value, inner, visit);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (isSeq(node)) {
|
|
162
|
+
for (const item of node.items) eachPair(item as Node, scope, visit);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Rewrite retired vocabulary in one raw YAML metadata document.
|
|
168
|
+
*
|
|
169
|
+
* Pure: no filesystem, no loader, no registry. Mirrors `rewriteDocument`'s contract exactly —
|
|
170
|
+
* same result shape, same scoping rule, same refusal policy — so `meta upgrade` reports a
|
|
171
|
+
* YAML estate and a JSON estate identically.
|
|
172
|
+
*/
|
|
173
|
+
export function rewriteYamlDocument(source: string, opts: RewriteOpts = {}): YamlRewriteResult {
|
|
174
|
+
const changes: RewriteChange[] = [];
|
|
175
|
+
const refusals: RewriteRefusal[] = [];
|
|
176
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
177
|
+
|
|
178
|
+
const lineCounter = new LineCounter();
|
|
179
|
+
const doc = parseDocument(source, { lineCounter, keepSourceTokens: true });
|
|
180
|
+
// A document we cannot parse is a document we must not edit. Reporting nothing here is
|
|
181
|
+
// correct: `meta verify` owns malformed YAML, and guessing at spans in a broken file is
|
|
182
|
+
// how a fixer corrupts one.
|
|
183
|
+
if (doc.errors.length > 0 || doc.contents == null) {
|
|
184
|
+
return { text: source, changes, refusals, unparseable: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const lineOf = (offset: number): number => lineCounter.linePos(offset).line;
|
|
188
|
+
const inWindow = (e: RetiredEntry): boolean =>
|
|
189
|
+
opts.maxVersion === undefined || atOrBefore(e.since, opts.maxVersion);
|
|
190
|
+
|
|
191
|
+
eachPair(doc.contents, undefined, (pair, scope, flow) => {
|
|
192
|
+
const key = keyText(pair.key);
|
|
193
|
+
if (key === undefined) return;
|
|
194
|
+
const span = pairSpan(pair);
|
|
195
|
+
if (span === undefined) return;
|
|
196
|
+
|
|
197
|
+
// A retired SUBTYPE has no attribute to rewrite — the node itself has to be re-modelled,
|
|
198
|
+
// which is the adopter's judgment. Reporting it is what keeps `meta upgrade` from exiting
|
|
199
|
+
// 0 on a document that still will not load.
|
|
200
|
+
if (TYPE_KEY.test(key)) {
|
|
201
|
+
for (const entry of RETIRED_VOCABULARY) {
|
|
202
|
+
if (entry.isSubTypeRetirement !== true || !inWindow(entry)) continue;
|
|
203
|
+
if (`${entry.type}.${entry.subType}` !== key) continue;
|
|
204
|
+
refusals.push({ ...note(entry), subject: key, line: lineOf(span.keyStart) });
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Sigil-free authoring is the norm, but an authored `@` must not make a retirement
|
|
210
|
+
// invisible.
|
|
211
|
+
const bare = key.startsWith("@") ? key.slice(1) : key;
|
|
212
|
+
const raw = source.slice(span.keyEnd, span.valueEnd).replace(/^\s*:\s*/, "").trim();
|
|
213
|
+
const line = lineOf(span.keyStart);
|
|
214
|
+
|
|
215
|
+
for (const entry of RETIRED_VOCABULARY) {
|
|
216
|
+
if (entry.attr !== bare || !inWindow(entry)) continue;
|
|
217
|
+
if (scope === undefined || !scopeMatches(entry, scope)) continue;
|
|
218
|
+
|
|
219
|
+
// A VALUE-scoped retirement only fires on the retired values — the same attribute with
|
|
220
|
+
// a live value must come through untouched.
|
|
221
|
+
if (entry.attrValues !== undefined && !entry.attrValues.some((v) => raw === v || raw === `"${v}"` || raw === `'${v}'`)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const refuse = (): void => {
|
|
226
|
+
refusals.push({ ...note(entry), subject: `@${bare}`, ...(raw !== "" ? { value: raw } : {}), line });
|
|
227
|
+
};
|
|
228
|
+
const drop = (): void => {
|
|
229
|
+
const d = dropSpan(source, span, flow);
|
|
230
|
+
// Undeletable in place (a sequence item's leading key) — report it instead of
|
|
231
|
+
// producing YAML that parses as something else.
|
|
232
|
+
if (d === undefined) {
|
|
233
|
+
refuse();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
edits.push({ ...d, text: "" });
|
|
237
|
+
changes.push({ attr: bare, from: bare, to: "(removed)", line });
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const rw = entry.rewrite;
|
|
241
|
+
if (rw === undefined) refuse();
|
|
242
|
+
else if (rw.kind === "renameAttr") {
|
|
243
|
+
// Preserve the author's quoting style; YAML keys are usually bare, but a quoted key
|
|
244
|
+
// must stay quoted or the surrounding style stops being self-consistent.
|
|
245
|
+
const rawKey = source.slice(span.keyStart, span.keyEnd);
|
|
246
|
+
const q = rawKey[0] === '"' || rawKey[0] === "'" ? rawKey[0] : "";
|
|
247
|
+
edits.push({ start: span.keyStart, end: span.keyEnd, text: `${q}${rw.to}${q}` });
|
|
248
|
+
changes.push({ attr: bare, from: bare, to: rw.to, line });
|
|
249
|
+
} else if (rw.kind === "dropAttr") drop();
|
|
250
|
+
else if (raw === String(rw.fromValue) || raw === `"${rw.fromValue}"` || raw === `'${rw.fromValue}'`) {
|
|
251
|
+
const valText = typeof rw.toValue === "string" ? String(rw.toValue) : JSON.stringify(rw.toValue);
|
|
252
|
+
edits.push({ start: span.keyStart, end: span.keyEnd, text: rw.toAttr });
|
|
253
|
+
edits.push({ start: span.keyEnd, end: span.valueEnd, text: `: ${valText}` });
|
|
254
|
+
changes.push({ attr: bare, from: `${bare}: ${raw}`, to: `${rw.toAttr}: ${valText}`, line });
|
|
255
|
+
} else if (rw.otherwise === "drop") drop();
|
|
256
|
+
else refuse();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Applied right-to-left against the ORIGINAL text: rewriting incrementally would invalidate
|
|
261
|
+
// every later offset.
|
|
262
|
+
edits.sort((a, b) => b.start - a.start);
|
|
263
|
+
let text = source;
|
|
264
|
+
for (const e of edits) text = text.slice(0, e.start) + e.text + text.slice(e.end);
|
|
265
|
+
|
|
266
|
+
return { text, changes, refusals, unparseable: false };
|
|
267
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -159,6 +159,23 @@ export const ERROR_CODES = [
|
|
|
159
159
|
// SP-H Unit9 — @filterable: true on a field subtype with no filter-operator
|
|
160
160
|
// band (e.g. field.object). Would silently generate an empty-ops filter.
|
|
161
161
|
"ERR_FILTERABLE_UNSUPPORTED_SUBTYPE",
|
|
162
|
+
// #335 Half B — @sortable: true on an array field or a subtype with no
|
|
163
|
+
// filter-operator band (e.g. field.object). Would silently emit a sort
|
|
164
|
+
// entry over a column no dialect can ORDER BY.
|
|
165
|
+
"ERR_SORTABLE_UNSUPPORTED_SUBTYPE",
|
|
166
|
+
// #335 Half A — a whole-object @agg:collect (no @of; the carrying field.object
|
|
167
|
+
// rolls related rows up as its declared @objectRef value object) is malformed:
|
|
168
|
+
// carrier is not a field.object with @objectRef, @via absent, @distinct declared
|
|
169
|
+
// (refused — a no-op whenever the value object carries the primary key), an
|
|
170
|
+
// @orderBy key not on the @via TERMINAL entity, or a member's declared type
|
|
171
|
+
// disagreeing with the matched terminal field's. Distinct from ERR_INVALID_ORIGIN
|
|
172
|
+
// so a fixture can tell this arm from a loader that still requires @of.
|
|
173
|
+
"ERR_COLLECT_WHOLE_OBJECT",
|
|
174
|
+
// #335 Half A — a whole-object @agg:collect's value-object member has no
|
|
175
|
+
// matching field (by name) on the @via terminal entity. The lowering
|
|
176
|
+
// projects exactly the declared members; failing open here is how #270
|
|
177
|
+
// turned a curated value object into the full entity.
|
|
178
|
+
"ERR_COLLECT_MEMBER_UNRESOLVED",
|
|
162
179
|
// ADR-0023 — a registration was attempted against a registry sealed after its
|
|
163
180
|
// agreed metamodel-provider bootstrap. Codegen cannot invent metamodel attrs.
|
|
164
181
|
"ERR_REGISTRY_SEALED",
|
|
@@ -174,8 +191,10 @@ export const ERROR_CODES = [
|
|
|
174
191
|
// clash / required-child cycle / conflicting attr redefinition. The detail names
|
|
175
192
|
// which of the six checks fired and the offending type(s).
|
|
176
193
|
"ERR_INVALID_METAMODEL_CONSTRAINT",
|
|
177
|
-
//
|
|
178
|
-
//
|
|
194
|
+
// Index-key resolution for index.lookup AND identity.secondary (#342) — the key is
|
|
195
|
+
// @fields XOR @expr: neither declared, BOTH declared (@expr is used INSTEAD of
|
|
196
|
+
// @fields), whichever is declared supplies no key, or a named field does not exist
|
|
197
|
+
// on the owning entity's effective (resolved via extends) field set.
|
|
179
198
|
"ERR_INVALID_INDEX",
|
|
180
199
|
// #195 — origin.computed @expr: the expression tree's inferred root type does
|
|
181
200
|
// not equal the carrying field's declared field.<subType>. A computed column's
|
|
@@ -18,7 +18,7 @@ import { ParseError } from "../errors.js";
|
|
|
18
18
|
import type { LoaderWarning } from "../source.js";
|
|
19
19
|
import { codeSource, resolvedSource } from "../source.js";
|
|
20
20
|
import { parseJson } from "../parser-json.js";
|
|
21
|
-
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter } from "./validation-passes.js";
|
|
21
|
+
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter } from "./validation-passes.js";
|
|
22
22
|
import { runRegisteredValidation } from "./validation-registry.js";
|
|
23
23
|
import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
|
|
24
24
|
import { validateSourceEscapes } from "../persistence/source/validate-source-escapes.js";
|
|
@@ -578,6 +578,10 @@ export class MetaDataLoader {
|
|
|
578
578
|
// (would silently generate a filter that rejects every request).
|
|
579
579
|
errors.push(...validateFilterableHasSupportedOps(root));
|
|
580
580
|
|
|
581
|
+
// #335 Half B — @sortable on an array field or a subtype with no operator
|
|
582
|
+
// band → error (would silently emit a sort entry no dialect can execute).
|
|
583
|
+
errors.push(...validateSortableHasSupportedSubtype(root));
|
|
584
|
+
|
|
581
585
|
// Sixth pass: origin path validation — validates passthrough.@from,
|
|
582
586
|
// aggregate.@of, and .@via relationship chains.
|
|
583
587
|
errors.push(...validateOriginPaths(root));
|