@metaobjectsdev/metadata 0.6.0 → 0.7.0-rc.10
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 +54 -3
- package/dist/attr-schema-validate.js +7 -7
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/export-json.d.ts +6 -7
- package/dist/core/export-json.d.ts.map +1 -1
- package/dist/core/export-json.js +15 -17
- package/dist/core/export-json.js.map +1 -1
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/parser-yaml.d.ts.map +1 -1
- package/dist/core/parser-yaml.js +36 -11
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/core/yaml-desugar.d.ts.map +1 -1
- package/dist/core/yaml-desugar.js +54 -5
- package/dist/core/yaml-desugar.js.map +1 -1
- package/dist/core/yaml-positions-walker.d.ts +21 -0
- package/dist/core/yaml-positions-walker.d.ts.map +1 -0
- package/dist/core/yaml-positions-walker.js +75 -0
- package/dist/core/yaml-positions-walker.js.map +1 -0
- package/dist/core/yaml-positions.d.ts +19 -0
- package/dist/core/yaml-positions.d.ts.map +1 -0
- package/dist/core/yaml-positions.js +60 -0
- package/dist/core/yaml-positions.js.map +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +7 -4
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +32 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/json-path.d.ts +8 -0
- package/dist/json-path.d.ts.map +1 -0
- package/dist/json-path.js +39 -0
- package/dist/json-path.js.map +1 -0
- package/dist/loader/meta-data-loader.d.ts +47 -6
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +126 -8
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/meta-data-source.d.ts +6 -2
- package/dist/loader/meta-data-source.d.ts.map +1 -1
- package/dist/loader/meta-data-source.js +10 -6
- package/dist/loader/meta-data-source.js.map +1 -1
- package/dist/loader/shortcuts.d.ts +9 -0
- package/dist/loader/shortcuts.d.ts.map +1 -0
- package/dist/loader/shortcuts.js +19 -0
- package/dist/loader/shortcuts.js.map +1 -0
- package/dist/loader/sources/directory-source.d.ts +15 -0
- package/dist/loader/sources/directory-source.d.ts.map +1 -0
- package/dist/loader/sources/directory-source.js +80 -0
- package/dist/loader/sources/directory-source.js.map +1 -0
- package/dist/loader/sources/file-source.d.ts +12 -0
- package/dist/loader/sources/file-source.d.ts.map +1 -0
- package/dist/loader/sources/file-source.js +46 -0
- package/dist/loader/sources/file-source.js.map +1 -0
- package/dist/loader/sources/index.d.ts +5 -0
- package/dist/loader/sources/index.d.ts.map +1 -0
- package/dist/loader/sources/index.js +5 -0
- package/dist/loader/sources/index.js.map +1 -0
- package/dist/loader/sources/uri-source.d.ts +9 -0
- package/dist/loader/sources/uri-source.d.ts.map +1 -0
- package/dist/loader/sources/uri-source.js +42 -0
- package/dist/loader/sources/uri-source.js.map +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +92 -28
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming.d.ts +15 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +20 -6
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts +17 -4
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +371 -44
- package/dist/parser-core.js.map +1 -1
- package/dist/parser-json.d.ts.map +1 -1
- package/dist/parser-json.js +10 -2
- package/dist/parser-json.js.map +1 -1
- package/dist/persistence/source/validate-source-roles.js +2 -2
- package/dist/persistence/source/validate-source-roles.js.map +1 -1
- package/dist/semantic-diff.d.ts +5 -0
- package/dist/semantic-diff.d.ts.map +1 -0
- package/dist/semantic-diff.js +49 -0
- package/dist/semantic-diff.js.map +1 -0
- 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 +23 -0
- package/dist/shared/meta-data.js.map +1 -1
- package/dist/source.d.ts +96 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +38 -0
- package/dist/source.js.map +1 -0
- package/dist/subtype-rules.js +1 -1
- package/dist/subtype-rules.js.map +1 -1
- package/dist/super-resolve.d.ts +2 -0
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +1 -1
- package/dist/super-resolve.js.map +1 -1
- package/dist/template/template-constants.d.ts +3 -1
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +22 -6
- 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 +41 -1
- package/dist/template/template-schema.js.map +1 -1
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +7 -7
- package/src/core/export-json.ts +15 -18
- package/src/core/index.ts +8 -2
- package/src/core/parser-yaml.ts +38 -11
- package/src/core/yaml-desugar.ts +58 -4
- package/src/core/yaml-positions-walker.ts +101 -0
- package/src/core/yaml-positions.ts +80 -0
- package/src/core-types.ts +7 -4
- package/src/errors.ts +57 -8
- package/src/index.ts +28 -3
- package/src/json-path.ts +46 -0
- package/src/loader/meta-data-loader.ts +168 -10
- package/src/loader/meta-data-source.ts +10 -6
- package/src/loader/shortcuts.ts +31 -0
- package/src/loader/sources/directory-source.ts +90 -0
- package/src/{core → loader/sources}/file-source.ts +3 -3
- package/src/loader/sources/index.ts +6 -0
- package/src/loader/sources/uri-source.ts +44 -0
- package/src/loader/validation-passes.ts +96 -29
- package/src/naming.ts +39 -7
- package/src/parser-core.ts +412 -46
- package/src/parser-json.ts +11 -2
- package/src/persistence/source/validate-source-roles.ts +2 -2
- package/src/semantic-diff.ts +48 -0
- package/src/shared/meta-data.ts +28 -0
- package/src/source.ts +99 -0
- package/src/subtype-rules.ts +1 -1
- package/src/super-resolve.ts +3 -1
- package/src/template/template-constants.ts +23 -6
- package/src/template/template-schema.ts +43 -0
- package/src/core/file-meta-data-loader.ts +0 -89
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// DirectorySource — expands a directory into a sorted list of FileSource.
|
|
2
|
+
//
|
|
3
|
+
// Discovers .json / .yaml / .yml files (case-insensitive on extension).
|
|
4
|
+
// Recurses by default — matches Java/Python/C# DirectorySource behavior.
|
|
5
|
+
// Sort order is ordinal-by-basename so the overlay merge is deterministic
|
|
6
|
+
// across environments and language ports.
|
|
7
|
+
|
|
8
|
+
import { readdir, stat } from "node:fs/promises";
|
|
9
|
+
import { basename, extname, join } from "node:path";
|
|
10
|
+
import { FileSource } from "./file-source.js";
|
|
11
|
+
import type { MetaDataSource } from "../meta-data-source.js";
|
|
12
|
+
|
|
13
|
+
export interface DirectoryOptions {
|
|
14
|
+
/** Filename patterns to exclude. Supports literal match and `*` / `**` globs. */
|
|
15
|
+
exclude?: string[];
|
|
16
|
+
/** Recurse into subdirectories. Default: true. */
|
|
17
|
+
recurse?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SUPPORTED_EXTS = new Set([".json", ".yaml", ".yml"]);
|
|
21
|
+
|
|
22
|
+
/** Minimal glob matcher supporting `*` (any chars except `/`) and `**` (any chars). */
|
|
23
|
+
function matchSimpleGlob(pattern: string, value: string): boolean {
|
|
24
|
+
const regexStr = pattern
|
|
25
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
26
|
+
.replace(/\*\*/g, "::DOUBLESTAR::")
|
|
27
|
+
.replace(/\*/g, "[^/]*")
|
|
28
|
+
.replace(/::DOUBLESTAR::/g, ".*");
|
|
29
|
+
return new RegExp(`^${regexStr}$`).test(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class DirectorySource {
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly directory: string,
|
|
35
|
+
public readonly opts: DirectoryOptions = {},
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
async expand(): Promise<MetaDataSource[]> {
|
|
39
|
+
const recurse = this.opts.recurse ?? true;
|
|
40
|
+
const exclude = this.opts.exclude ?? [];
|
|
41
|
+
|
|
42
|
+
const files = await this.collect(this.directory, recurse);
|
|
43
|
+
const filtered: string[] = [];
|
|
44
|
+
for (const p of files) {
|
|
45
|
+
if (!SUPPORTED_EXTS.has(extname(p).toLowerCase())) continue;
|
|
46
|
+
const name = basename(p);
|
|
47
|
+
if (exclude.some((pat) => matchSimpleGlob(pat, name))) continue;
|
|
48
|
+
filtered.push(p);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Sort by basename (ordinal) for deterministic overlay order. The
|
|
52
|
+
// pre-unification FileMetaDataLoader sorted readdir entries (basenames)
|
|
53
|
+
// for the same reason — that contract carries forward here.
|
|
54
|
+
filtered.sort((a, b) => {
|
|
55
|
+
const an = basename(a);
|
|
56
|
+
const bn = basename(b);
|
|
57
|
+
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return filtered.map((p) => new FileSource(p));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async collect(dir: string, recurse: boolean): Promise<string[]> {
|
|
64
|
+
let entries: string[];
|
|
65
|
+
try {
|
|
66
|
+
entries = await readdir(dir);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`DirectorySource: cannot read ${dir}: ${(err as Error).message}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const out: string[] = [];
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const full = join(dir, entry);
|
|
75
|
+
let s;
|
|
76
|
+
try {
|
|
77
|
+
s = await stat(full);
|
|
78
|
+
} catch {
|
|
79
|
+
// Entry vanished between readdir and stat (TOCTOU) or is not accessible.
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (s.isDirectory()) {
|
|
83
|
+
if (recurse) out.push(...(await this.collect(full, recurse)));
|
|
84
|
+
} else if (s.isFile()) {
|
|
85
|
+
out.push(full);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// FileSource — a MetaDataSource backed by a file on disk. Server-side only
|
|
2
|
-
// (touches node:fs); lives under src/
|
|
3
|
-
//
|
|
2
|
+
// (touches node:fs); lives under src/loader/sources/ alongside the other
|
|
3
|
+
// MetaDataSource implementations.
|
|
4
4
|
|
|
5
5
|
import { basename, extname } from "node:path";
|
|
6
|
-
import type { MetaDataFormat, MetaDataSource } from "../
|
|
6
|
+
import type { MetaDataFormat, MetaDataSource } from "../meta-data-source.js";
|
|
7
7
|
|
|
8
8
|
/** Infer a source format from a file extension. `.yaml`/`.yml` → "yaml";
|
|
9
9
|
* everything else (including `.json`) → "json", the canonical default. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Barrel re-export for the MetaDataSource implementations.
|
|
2
|
+
|
|
3
|
+
export { FileSource } from "./file-source.js";
|
|
4
|
+
export { DirectorySource } from "./directory-source.js";
|
|
5
|
+
export type { DirectoryOptions } from "./directory-source.js";
|
|
6
|
+
export { UriSource } from "./uri-source.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// UriSource — a MetaDataSource that fetches content from a URI.
|
|
2
|
+
// Supports file://, http://, and https:// schemes.
|
|
3
|
+
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { extname } from "node:path";
|
|
7
|
+
import type { MetaDataFormat, MetaDataSource } from "../meta-data-source.js";
|
|
8
|
+
|
|
9
|
+
function inferFormat(uri: string): MetaDataFormat {
|
|
10
|
+
// URL constructor handles file://, http://, https:// uniformly.
|
|
11
|
+
// Strip query/fragment by reading .pathname.
|
|
12
|
+
try {
|
|
13
|
+
const pathname = new URL(uri).pathname;
|
|
14
|
+
const ext = extname(pathname).toLowerCase();
|
|
15
|
+
return ext === ".yaml" || ext === ".yml" ? "yaml" : "json";
|
|
16
|
+
} catch {
|
|
17
|
+
// If the URI isn't a valid URL, fall back to JSON.
|
|
18
|
+
return "json";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class UriSource implements MetaDataSource {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly format: MetaDataFormat;
|
|
25
|
+
|
|
26
|
+
constructor(public readonly uri: string, format?: MetaDataFormat) {
|
|
27
|
+
this.id = uri;
|
|
28
|
+
this.format = format ?? inferFormat(uri);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async read(): Promise<string> {
|
|
32
|
+
if (this.uri.startsWith("file://")) {
|
|
33
|
+
return readFile(fileURLToPath(this.uri), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
if (this.uri.startsWith("http://") || this.uri.startsWith("https://")) {
|
|
36
|
+
const res = await fetch(this.uri);
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(`UriSource: ${this.uri} -> HTTP ${res.status}`);
|
|
39
|
+
}
|
|
40
|
+
return res.text();
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`UriSource: unsupported scheme on ${this.uri}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { MetaData } from "../shared/meta-data.js";
|
|
13
13
|
import { ParseError } from "../errors.js";
|
|
14
|
+
import { resolvedSource, type ErrorSource } from "../source.js";
|
|
14
15
|
import {
|
|
15
16
|
TYPE_OBJECT,
|
|
16
17
|
TYPE_FIELD,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
25
26
|
TEMPLATE_ATTR_REQUIRED_SLOTS,
|
|
26
27
|
} from "../template/template-constants.js";
|
|
28
|
+
import { OBJECT_SUBTYPE_VALUE } from "../core/object/object-constants.js";
|
|
27
29
|
import {
|
|
28
30
|
LAYOUT_SUBTYPE_DATA_GRID,
|
|
29
31
|
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
|
|
@@ -72,7 +74,7 @@ export function validateDataGridSortFields(root: MetaData): ParseError[] {
|
|
|
72
74
|
new ParseError(
|
|
73
75
|
`dataGrid layout "${layout.name}" on entity "${obj.name}" has @defaultSortField "${sortField}" ` +
|
|
74
76
|
`but no such field exists on "${obj.name}". Available fields: ${[...fieldNames].join(", ")}`,
|
|
75
|
-
{ code: "ERR_BAD_DEFAULT_SORT_FIELD" },
|
|
77
|
+
{ code: "ERR_BAD_DEFAULT_SORT_FIELD", source: layout.source },
|
|
76
78
|
),
|
|
77
79
|
);
|
|
78
80
|
}
|
|
@@ -98,11 +100,16 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
|
98
100
|
const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
99
101
|
if (typeof payloadRef !== "string") continue; // absence handled by the required-attr schema check
|
|
100
102
|
const payload = root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === payloadRef);
|
|
101
|
-
if (!payload) {
|
|
103
|
+
if (!payload || payload.subType !== OBJECT_SUBTYPE_VALUE) {
|
|
104
|
+
// FR5d — @payloadRef is a reference; emit format=resolved with
|
|
105
|
+
// referrer=template FQN, target=the unresolved payloadRef string.
|
|
102
106
|
errors.push(
|
|
103
107
|
new ParseError(
|
|
104
|
-
`template "${tmpl.name}" @payloadRef "${payloadRef}" does not resolve to
|
|
105
|
-
{
|
|
108
|
+
`template "${tmpl.name}" @payloadRef "${payloadRef}" does not resolve to an object.value at root`,
|
|
109
|
+
{
|
|
110
|
+
code: "ERR_INVALID_TEMPLATE",
|
|
111
|
+
source: resolvedSource(tmpl.source, tmpl.fqn(), payloadRef),
|
|
112
|
+
},
|
|
106
113
|
),
|
|
107
114
|
);
|
|
108
115
|
continue;
|
|
@@ -114,11 +121,17 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
|
114
121
|
const slotList = Array.isArray(slots) ? slots : typeof slots === "string" ? [slots] : [];
|
|
115
122
|
for (const slot of slotList) {
|
|
116
123
|
if (typeof slot === "string" && !fieldNames.has(slot)) {
|
|
124
|
+
// FR5d — @requiredSlots is a field-on-payload reference; emit
|
|
125
|
+
// format=resolved with referrer=template FQN, target=`payloadRef.slot`
|
|
126
|
+
// (the dotted ref that did not resolve to a payload field).
|
|
117
127
|
errors.push(
|
|
118
128
|
new ParseError(
|
|
119
129
|
`template "${tmpl.name}" @requiredSlots "${slot}" is not a field on payload "${payloadRef}". ` +
|
|
120
130
|
`Available fields: ${[...fieldNames].join(", ")}`,
|
|
121
|
-
{
|
|
131
|
+
{
|
|
132
|
+
code: "ERR_INVALID_TEMPLATE",
|
|
133
|
+
source: resolvedSource(tmpl.source, tmpl.fqn(), `${payloadRef}.${slot}`),
|
|
134
|
+
},
|
|
122
135
|
),
|
|
123
136
|
);
|
|
124
137
|
}
|
|
@@ -194,17 +207,28 @@ function _findRelationship(obj: MetaData, name: string): MetaData | undefined {
|
|
|
194
207
|
function _validateFromPath(
|
|
195
208
|
fromAttr: string,
|
|
196
209
|
root: MetaData,
|
|
197
|
-
|
|
210
|
+
projection: MetaData,
|
|
198
211
|
fieldName: string,
|
|
212
|
+
originSource: ErrorSource,
|
|
199
213
|
errors: ParseError[],
|
|
200
214
|
label: string = "origin.passthrough.@from",
|
|
201
215
|
): void {
|
|
216
|
+
const projectionName = projection.name;
|
|
217
|
+
// FR5d — referrer is `<projection-FQN>::<fieldName>` (the canonical
|
|
218
|
+
// "where the broken reference lives" identifier).
|
|
219
|
+
const referrer = `${projection.fqn()}::${fieldName}`;
|
|
202
220
|
const dotIdx = fromAttr.indexOf(".");
|
|
203
221
|
if (dotIdx < 1 || dotIdx === fromAttr.length - 1) {
|
|
222
|
+
// Malformed shape (not "Entity.field") — not a reference resolution
|
|
223
|
+
// failure per se, but emit format=resolved with target=the bad string
|
|
224
|
+
// so consumers see the same envelope shape across all FR5d sites.
|
|
204
225
|
errors.push(
|
|
205
226
|
new ParseError(
|
|
206
227
|
`${label} "${fromAttr}" on ${projectionName}.${fieldName}: must be of form "Entity.field".`,
|
|
207
|
-
{
|
|
228
|
+
{
|
|
229
|
+
code: "ERR_INVALID_ORIGIN",
|
|
230
|
+
source: resolvedSource(originSource, referrer, fromAttr),
|
|
231
|
+
},
|
|
208
232
|
),
|
|
209
233
|
);
|
|
210
234
|
return;
|
|
@@ -213,20 +237,28 @@ function _validateFromPath(
|
|
|
213
237
|
const targetFieldName = fromAttr.slice(dotIdx + 1);
|
|
214
238
|
const sourceObj = _findObject(root, entityName);
|
|
215
239
|
if (!sourceObj) {
|
|
240
|
+
// FR5d — entity half of the ref didn't resolve. target = full ref.
|
|
216
241
|
errors.push(
|
|
217
242
|
new ParseError(
|
|
218
243
|
`${label} "${fromAttr}" on ${projectionName}.${fieldName}: no such entity "${entityName}".`,
|
|
219
|
-
{
|
|
244
|
+
{
|
|
245
|
+
code: "ERR_INVALID_ORIGIN",
|
|
246
|
+
source: resolvedSource(originSource, referrer, fromAttr),
|
|
247
|
+
},
|
|
220
248
|
),
|
|
221
249
|
);
|
|
222
250
|
return;
|
|
223
251
|
}
|
|
224
252
|
const sourceField = _findField(sourceObj, targetFieldName);
|
|
225
253
|
if (!sourceField) {
|
|
254
|
+
// FR5d — entity resolved, field on it did not. target = full ref.
|
|
226
255
|
errors.push(
|
|
227
256
|
new ParseError(
|
|
228
257
|
`${label} "${fromAttr}" on ${projectionName}.${fieldName}: no such field "${targetFieldName}" on ${entityName}.`,
|
|
229
|
-
{
|
|
258
|
+
{
|
|
259
|
+
code: "ERR_INVALID_ORIGIN",
|
|
260
|
+
source: resolvedSource(originSource, referrer, fromAttr),
|
|
261
|
+
},
|
|
230
262
|
),
|
|
231
263
|
);
|
|
232
264
|
}
|
|
@@ -235,16 +267,23 @@ function _validateFromPath(
|
|
|
235
267
|
function _validateViaPath(
|
|
236
268
|
viaAttr: string,
|
|
237
269
|
root: MetaData,
|
|
238
|
-
|
|
270
|
+
projection: MetaData,
|
|
239
271
|
fieldName: string,
|
|
272
|
+
originSource: ErrorSource,
|
|
240
273
|
errors: ParseError[],
|
|
241
274
|
): void {
|
|
275
|
+
const projectionName = projection.name;
|
|
276
|
+
// FR5d — referrer is `<projection-FQN>::<fieldName>`.
|
|
277
|
+
const referrer = `${projection.fqn()}::${fieldName}`;
|
|
242
278
|
const segments = viaAttr.split(".");
|
|
243
279
|
if (segments.length < 2) {
|
|
244
280
|
errors.push(
|
|
245
281
|
new ParseError(
|
|
246
282
|
`origin.@via "${viaAttr}" on ${projectionName}.${fieldName}: must be of form "Entity.relationship[.relationship...]".`,
|
|
247
|
-
{
|
|
283
|
+
{
|
|
284
|
+
code: "ERR_INVALID_ORIGIN",
|
|
285
|
+
source: resolvedSource(originSource, referrer, viaAttr),
|
|
286
|
+
},
|
|
248
287
|
),
|
|
249
288
|
);
|
|
250
289
|
return;
|
|
@@ -255,18 +294,32 @@ function _validateViaPath(
|
|
|
255
294
|
errors.push(
|
|
256
295
|
new ParseError(
|
|
257
296
|
`origin.@via "${viaAttr}" on ${projectionName}.${fieldName}: no such entity "${entityName}".`,
|
|
258
|
-
{
|
|
297
|
+
{
|
|
298
|
+
code: "ERR_INVALID_ORIGIN",
|
|
299
|
+
source: resolvedSource(originSource, referrer, viaAttr),
|
|
300
|
+
},
|
|
259
301
|
),
|
|
260
302
|
);
|
|
261
303
|
return;
|
|
262
304
|
}
|
|
305
|
+
// FR5d — track the deepest-valid-prefix as we walk. The prefix grows
|
|
306
|
+
// segment-by-segment; on a hop failure the error message names the prefix
|
|
307
|
+
// that DID resolve, so authors can fix multi-hop typos quickly.
|
|
308
|
+
// After the entity lookup above, the deepest valid prefix is just the
|
|
309
|
+
// entity name; each successful relationship hop appends a segment.
|
|
310
|
+
const validSegments: string[] = [entityName];
|
|
263
311
|
for (const relName of relSegments) {
|
|
264
312
|
const rel = _findRelationship(currentObj, relName);
|
|
265
313
|
if (!rel) {
|
|
314
|
+
const prefix = validSegments.join(".");
|
|
266
315
|
errors.push(
|
|
267
316
|
new ParseError(
|
|
268
|
-
`origin.@via "${viaAttr}" on ${projectionName}.${fieldName}: no such relationship "${relName}" on ${currentObj.name}
|
|
269
|
-
|
|
317
|
+
`origin.@via "${viaAttr}" on ${projectionName}.${fieldName}: no such relationship "${relName}" on ${currentObj.name}. ` +
|
|
318
|
+
`Deepest valid prefix was "${prefix}".`,
|
|
319
|
+
{
|
|
320
|
+
code: "ERR_INVALID_ORIGIN",
|
|
321
|
+
source: resolvedSource(originSource, referrer, viaAttr),
|
|
322
|
+
},
|
|
270
323
|
),
|
|
271
324
|
);
|
|
272
325
|
return;
|
|
@@ -276,21 +329,32 @@ function _validateViaPath(
|
|
|
276
329
|
errors.push(
|
|
277
330
|
new ParseError(
|
|
278
331
|
`origin.@via "${viaAttr}" on ${projectionName}.${fieldName}: relationship "${relName}" on ${currentObj.name} is missing @objectRef.`,
|
|
279
|
-
{
|
|
332
|
+
{
|
|
333
|
+
code: "ERR_INVALID_ORIGIN",
|
|
334
|
+
source: resolvedSource(originSource, referrer, viaAttr),
|
|
335
|
+
},
|
|
280
336
|
),
|
|
281
337
|
);
|
|
282
338
|
return;
|
|
283
339
|
}
|
|
284
340
|
const nextObj = _findObject(root, refTarget);
|
|
285
341
|
if (!nextObj) {
|
|
342
|
+
// FR5d — relationship's @objectRef points at a missing entity. This
|
|
343
|
+
// is the @objectRef-resolution edge of the via-path walk (the "5th
|
|
344
|
+
// site" in FR5d's scope list for @objectRef references encountered
|
|
345
|
+
// transitively).
|
|
286
346
|
errors.push(
|
|
287
347
|
new ParseError(
|
|
288
348
|
`origin.@via "${viaAttr}" on ${projectionName}.${fieldName}: relationship "${relName}" points to non-existent entity "${refTarget}".`,
|
|
289
|
-
{
|
|
349
|
+
{
|
|
350
|
+
code: "ERR_INVALID_ORIGIN",
|
|
351
|
+
source: resolvedSource(originSource, referrer, refTarget),
|
|
352
|
+
},
|
|
290
353
|
),
|
|
291
354
|
);
|
|
292
355
|
return;
|
|
293
356
|
}
|
|
357
|
+
validSegments.push(relName);
|
|
294
358
|
currentObj = nextObj;
|
|
295
359
|
}
|
|
296
360
|
}
|
|
@@ -303,18 +367,20 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
303
367
|
if (origin.subType === ORIGIN_SUBTYPE_PASSTHROUGH) {
|
|
304
368
|
const from = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_FROM);
|
|
305
369
|
if (typeof from !== "string" || from === "") {
|
|
370
|
+
// Missing-attr (not a reference resolution failure) — keep the
|
|
371
|
+
// node's own source envelope (json/yaml/merged).
|
|
306
372
|
errors.push(
|
|
307
373
|
new ParseError(
|
|
308
374
|
`origin.passthrough on ${obj.name}.${field.name}: missing @from.`,
|
|
309
|
-
{ code: "ERR_INVALID_ORIGIN" },
|
|
375
|
+
{ code: "ERR_INVALID_ORIGIN", source: origin.source },
|
|
310
376
|
),
|
|
311
377
|
);
|
|
312
378
|
continue;
|
|
313
379
|
}
|
|
314
|
-
_validateFromPath(from, root, obj
|
|
380
|
+
_validateFromPath(from, root, obj, field.name, origin.source, errors);
|
|
315
381
|
const via = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA);
|
|
316
382
|
if (typeof via === "string" && via !== "") {
|
|
317
|
-
_validateViaPath(via, root, obj
|
|
383
|
+
_validateViaPath(via, root, obj, field.name, origin.source, errors);
|
|
318
384
|
}
|
|
319
385
|
} else if (origin.subType === ORIGIN_SUBTYPE_AGGREGATE) {
|
|
320
386
|
const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF);
|
|
@@ -322,23 +388,23 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
322
388
|
errors.push(
|
|
323
389
|
new ParseError(
|
|
324
390
|
`origin.aggregate on ${obj.name}.${field.name}: missing @of.`,
|
|
325
|
-
{ code: "ERR_INVALID_ORIGIN" },
|
|
391
|
+
{ code: "ERR_INVALID_ORIGIN", source: origin.source },
|
|
326
392
|
),
|
|
327
393
|
);
|
|
328
394
|
continue;
|
|
329
395
|
}
|
|
330
|
-
_validateFromPath(of_, root, obj
|
|
396
|
+
_validateFromPath(of_, root, obj, field.name, origin.source, errors, "origin.aggregate.@of");
|
|
331
397
|
const via = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA);
|
|
332
398
|
if (typeof via !== "string" || via === "") {
|
|
333
399
|
errors.push(
|
|
334
400
|
new ParseError(
|
|
335
401
|
`origin.aggregate on ${obj.name}.${field.name}: missing @via (aggregates require a relationship path).`,
|
|
336
|
-
{ code: "ERR_INVALID_ORIGIN" },
|
|
402
|
+
{ code: "ERR_INVALID_ORIGIN", source: origin.source },
|
|
337
403
|
),
|
|
338
404
|
);
|
|
339
405
|
continue;
|
|
340
406
|
}
|
|
341
|
-
_validateViaPath(via, root, obj
|
|
407
|
+
_validateViaPath(via, root, obj, field.name, origin.source, errors);
|
|
342
408
|
}
|
|
343
409
|
}
|
|
344
410
|
}
|
|
@@ -371,7 +437,7 @@ export function validateFieldObjectStorage(root: MetaData): ParseError[] {
|
|
|
371
437
|
errors.push(
|
|
372
438
|
new ParseError(
|
|
373
439
|
`field "${obj.name}.${field.name}" sets @storage but has no @objectRef`,
|
|
374
|
-
{ code: "ERR_STORAGE_WITHOUT_OBJECT_REF" },
|
|
440
|
+
{ code: "ERR_STORAGE_WITHOUT_OBJECT_REF", source: field.source },
|
|
375
441
|
),
|
|
376
442
|
);
|
|
377
443
|
}
|
|
@@ -379,7 +445,7 @@ export function validateFieldObjectStorage(root: MetaData): ParseError[] {
|
|
|
379
445
|
errors.push(
|
|
380
446
|
new ParseError(
|
|
381
447
|
`field "${obj.name}.${field.name}" sets @storage "flattened" with isArray=true; flattened storage requires a single nested value`,
|
|
382
|
-
{ code: "ERR_STORAGE_FLATTENED_ARRAY" },
|
|
448
|
+
{ code: "ERR_STORAGE_FLATTENED_ARRAY", source: field.source },
|
|
383
449
|
),
|
|
384
450
|
);
|
|
385
451
|
}
|
|
@@ -413,7 +479,7 @@ export function validateDataGridFilterValues(root: MetaData): ParseError[] {
|
|
|
413
479
|
const filter = layout.ownAttr(LAYOUT_DATA_GRID_ATTR_FILTER);
|
|
414
480
|
// Type errors (e.g. legacy string form) are reported by validateAttrSchema.
|
|
415
481
|
if (typeof filter !== "object" || filter === null || Array.isArray(filter)) continue;
|
|
416
|
-
checkFilterClauses(filter as Record<string, unknown>, allow, obj.name, layout.name, errors);
|
|
482
|
+
checkFilterClauses(filter as Record<string, unknown>, allow, obj.name, layout.name, layout.source, errors);
|
|
417
483
|
}
|
|
418
484
|
}
|
|
419
485
|
return errors;
|
|
@@ -424,6 +490,7 @@ function checkFilterClauses(
|
|
|
424
490
|
allow: Map<string, readonly string[]>,
|
|
425
491
|
entityName: string,
|
|
426
492
|
layoutName: string,
|
|
493
|
+
layoutSource: ErrorSource,
|
|
427
494
|
errors: ParseError[],
|
|
428
495
|
): void {
|
|
429
496
|
for (const [key, clause] of Object.entries(filter)) {
|
|
@@ -431,7 +498,7 @@ function checkFilterClauses(
|
|
|
431
498
|
if (Array.isArray(clause)) {
|
|
432
499
|
for (const sub of clause) {
|
|
433
500
|
if (typeof sub === "object" && sub !== null && !Array.isArray(sub)) {
|
|
434
|
-
checkFilterClauses(sub as Record<string, unknown>, allow, entityName, layoutName, errors);
|
|
501
|
+
checkFilterClauses(sub as Record<string, unknown>, allow, entityName, layoutName, layoutSource, errors);
|
|
435
502
|
}
|
|
436
503
|
}
|
|
437
504
|
}
|
|
@@ -443,7 +510,7 @@ function checkFilterClauses(
|
|
|
443
510
|
new ParseError(
|
|
444
511
|
`dataGrid layout "${layoutName}" on entity "${entityName}" has @filter over ` +
|
|
445
512
|
`non-filterable field "${key}". Filterable fields: ${[...allow.keys()].join(", ") || "(none)"}`,
|
|
446
|
-
{ code: "ERR_BAD_ATTR_FILTER" },
|
|
513
|
+
{ code: "ERR_BAD_ATTR_FILTER", source: layoutSource },
|
|
447
514
|
),
|
|
448
515
|
);
|
|
449
516
|
continue;
|
|
@@ -457,7 +524,7 @@ function checkFilterClauses(
|
|
|
457
524
|
new ParseError(
|
|
458
525
|
`dataGrid layout "${layoutName}" on entity "${entityName}" @filter uses disallowed ` +
|
|
459
526
|
`op "${key}.${op}". Allowed ops for "${key}": ${allowedOps.join(", ")}`,
|
|
460
|
-
{ code: "ERR_BAD_ATTR_FILTER" },
|
|
527
|
+
{ code: "ERR_BAD_ATTR_FILTER", source: layoutSource },
|
|
461
528
|
),
|
|
462
529
|
);
|
|
463
530
|
}
|
package/src/naming.ts
CHANGED
|
@@ -28,6 +28,31 @@ export function toSnakeCase(s: string): string {
|
|
|
28
28
|
.toLowerCase();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function toKebabCase(s: string): string {
|
|
32
|
+
return toSnakeCase(s).replace(/_/g, "-");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Column-naming strategy applied by the persistence layer to fields with no
|
|
37
|
+
* explicit `@column` override. Persistence-layer config (set on
|
|
38
|
+
* ObjectManager / buildExpectedSchema / codegen config), not a metadata attr —
|
|
39
|
+
* the same metadata can drive snake_case (PG convention) or literal (EF
|
|
40
|
+
* convention) consumers. TS port defaults to snake_case; C# port defaults to
|
|
41
|
+
* literal.
|
|
42
|
+
*/
|
|
43
|
+
export type ColumnNamingStrategy = "snake_case" | "literal" | "kebab-case";
|
|
44
|
+
|
|
45
|
+
/** Single source of truth for the TS-port default. */
|
|
46
|
+
export const DEFAULT_COLUMN_NAMING_STRATEGY: ColumnNamingStrategy = "snake_case";
|
|
47
|
+
|
|
48
|
+
export function applyColumnNamingStrategy(name: string, strategy: ColumnNamingStrategy): string {
|
|
49
|
+
switch (strategy) {
|
|
50
|
+
case "literal": return name;
|
|
51
|
+
case "kebab-case": return toKebabCase(name);
|
|
52
|
+
case "snake_case": return toSnakeCase(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
export function pluralize(s: string): string {
|
|
32
57
|
if (/(s|x|z|ch|sh)$/i.test(s)) return s + "es";
|
|
33
58
|
if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
|
|
@@ -35,20 +60,24 @@ export function pluralize(s: string): string {
|
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
export function resolveTableName(entity: MetaData): string {
|
|
38
|
-
// Primary
|
|
63
|
+
// Primary source carries the physical table/view name (@table). Writability
|
|
64
|
+
// (table vs view/storedProc/tableFunction) only affects write-routing — for
|
|
65
|
+
// SELECT-side name resolution, a read-only primary source is the right answer.
|
|
39
66
|
const source = entity.ownChildren().find(
|
|
40
|
-
(c): c is MetaSource =>
|
|
41
|
-
c instanceof MetaSource && c.isWritable() && c.role === SOURCE_ROLE_PRIMARY,
|
|
67
|
+
(c): c is MetaSource => c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
|
|
42
68
|
);
|
|
43
69
|
const name = source?.tableName;
|
|
44
70
|
if (typeof name === "string" && name !== "") return name;
|
|
45
71
|
return pluralize(toSnakeCase(entity.name));
|
|
46
72
|
}
|
|
47
73
|
|
|
48
|
-
export function resolveColumnName(
|
|
74
|
+
export function resolveColumnName(
|
|
75
|
+
field: MetaData,
|
|
76
|
+
strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
77
|
+
): string {
|
|
49
78
|
const col = field.ownAttr(FIELD_ATTR_COLUMN);
|
|
50
79
|
if (typeof col === "string" && col) return col;
|
|
51
|
-
return
|
|
80
|
+
return applyColumnNamingStrategy(field.name, strategy);
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
/**
|
|
@@ -75,12 +104,15 @@ export interface EntityNameMap {
|
|
|
75
104
|
dbToJs: Map<string, string>;
|
|
76
105
|
}
|
|
77
106
|
|
|
78
|
-
export function buildNameMap(
|
|
107
|
+
export function buildNameMap(
|
|
108
|
+
entity: MetaData,
|
|
109
|
+
strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
110
|
+
): EntityNameMap {
|
|
79
111
|
const jsToDb = new Map<string, string>();
|
|
80
112
|
const dbToJs = new Map<string, string>();
|
|
81
113
|
for (const child of entity.ownChildren()) {
|
|
82
114
|
if (child.type !== TYPE_FIELD) continue;
|
|
83
|
-
const dbCol = resolveColumnName(child);
|
|
115
|
+
const dbCol = resolveColumnName(child, strategy);
|
|
84
116
|
jsToDb.set(child.name, dbCol);
|
|
85
117
|
dbToJs.set(dbCol, child.name);
|
|
86
118
|
}
|