@dforge-core/metadata 0.0.22 → 0.0.24
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/CHANGELOG.md +201 -0
- package/README.md +45 -0
- package/dist/actions-DvYTrCbP.d.ts +60 -0
- package/dist/dsl/index.d.ts +251 -0
- package/dist/dsl/index.js +1426 -0
- package/dist/dsl/index.js.map +1 -0
- package/dist/index.d.ts +26 -62
- package/dist/index.js +17 -8
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
- package/schemas/jobs.schema.json +2 -2
- package/src/dsl/builtins.ts +210 -0
- package/src/dsl/check.ts +813 -0
- package/src/dsl/index.ts +37 -0
- package/src/dsl/lexer.ts +394 -0
- package/src/dsl/parse.ts +713 -0
- package/src/dsl/types.ts +98 -0
- package/src/index.ts +1 -0
- package/src/traits.ts +49 -9
- package/src/types.ts +15 -0
package/src/dsl/types.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Host-agnostic types for the DSL checker.
|
|
2
|
+
//
|
|
3
|
+
// The checker reports OFFSETS plus a 1-indexed line/column, and never an editor
|
|
4
|
+
// range: a language server turns offsets into `Position`s with its own document
|
|
5
|
+
// model, while a validator prints `logic/actions/x.dsl:12`. Emitting both keeps
|
|
6
|
+
// either host from having to re-derive the other.
|
|
7
|
+
|
|
8
|
+
import type { ActionExecutionMode } from "../actions";
|
|
9
|
+
|
|
10
|
+
export type DslSeverity = "error" | "warning" | "info";
|
|
11
|
+
|
|
12
|
+
/** One reported problem. `rule` is stable — hosts filter and suppress on it. */
|
|
13
|
+
export interface DslIssue {
|
|
14
|
+
/** Stable rule id, namespaced: `dsl/unknown-column`. */
|
|
15
|
+
rule: string;
|
|
16
|
+
severity: DslSeverity;
|
|
17
|
+
message: string;
|
|
18
|
+
/** Byte offsets into the source, for range-based hosts. */
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
/** 1-indexed position of `start`, for line-based reporters. */
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Column membership test. Structurally satisfied by both `Set<string>` and
|
|
28
|
+
* `Map<string, unknown>`, so a host that already holds a column map passes it
|
|
29
|
+
* straight in rather than copying it into a set on every check.
|
|
30
|
+
*/
|
|
31
|
+
export interface ColumnLookup {
|
|
32
|
+
has(name: string): boolean;
|
|
33
|
+
readonly size: number;
|
|
34
|
+
/**
|
|
35
|
+
* The column names, if the lookup can enumerate them — `Set` and `Map` both
|
|
36
|
+
* can. Matching is case-insensitive at the other end (`EntityColumnLookup`
|
|
37
|
+
* builds an OrdinalIgnoreCase set) and a mixed-case column name is legal, so
|
|
38
|
+
* the checker folds case itself when it can read the keys.
|
|
39
|
+
*
|
|
40
|
+
* Without it, a lookup that is not already case-insensitive will report a
|
|
41
|
+
* spelling the server accepts. A host that cannot expose keys should make
|
|
42
|
+
* `has` case-insensitive instead.
|
|
43
|
+
*/
|
|
44
|
+
keys?(): Iterable<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The resolved entity a script's record context refers to. */
|
|
48
|
+
export interface EntityShape {
|
|
49
|
+
/** Module-qualified, e.g. `fin.invoice` — used in messages. */
|
|
50
|
+
qualified: string;
|
|
51
|
+
/**
|
|
52
|
+
* Every declared name on the entity, **trait-expanded** — built-in traits
|
|
53
|
+
* and the module's own `traits.json` alike — and including the virtual
|
|
54
|
+
* columns (reference, set, formula), exactly as `EntityColumnLookup` builds
|
|
55
|
+
* the compiler's set.
|
|
56
|
+
*
|
|
57
|
+
* Handing over an unexpanded set is the one mistake that turns this rule
|
|
58
|
+
* into a wall of false errors: the identity PK and `[status]` live in
|
|
59
|
+
* traits, not in `fields`. When in doubt pass `currentEntity: null` — a
|
|
60
|
+
* skipped check costs an install round trip, a wrong one blocks a pack.
|
|
61
|
+
*/
|
|
62
|
+
columns: ColumnLookup;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What the checker knows about the script's surroundings. Every field is
|
|
67
|
+
* optional and every rule that reads one FAILS OPEN: with an empty context the
|
|
68
|
+
* text-only rules still run and the rest stand down silently.
|
|
69
|
+
*
|
|
70
|
+
* That is deliberate. A false "unknown column" on a correct script is far more
|
|
71
|
+
* damaging than a missed one — an editor would underline working code, and a
|
|
72
|
+
* validator would block a pack — so a rule that cannot resolve what it needs
|
|
73
|
+
* reports nothing.
|
|
74
|
+
*/
|
|
75
|
+
export interface DslContext {
|
|
76
|
+
/** Owning module code, for the qualify-your-entity-codes message. */
|
|
77
|
+
moduleCode?: string;
|
|
78
|
+
/** The action this script implements, from `ui/actions.json`. */
|
|
79
|
+
action?: {
|
|
80
|
+
code: string;
|
|
81
|
+
executionMode?: ActionExecutionMode;
|
|
82
|
+
/**
|
|
83
|
+
* True when a scheduled job invokes this action. A job runs as the
|
|
84
|
+
* system user with no current record, so record context is a hard
|
|
85
|
+
* error there even in a mode that would otherwise allow it. Only a
|
|
86
|
+
* reader that has the module's jobs can know this, so it is the host's
|
|
87
|
+
* to supply; absent, the rule stands down like every other.
|
|
88
|
+
*/
|
|
89
|
+
viaJob?: boolean;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Entity behind the record context (`[field]`). `null` or absent when it
|
|
93
|
+
* can't be resolved — a bridge module acting on another module's entity —
|
|
94
|
+
* and then every column rule stands down. A partial set is worse than
|
|
95
|
+
* none: pass `null`, not the fragment. See `EntityShape.columns`.
|
|
96
|
+
*/
|
|
97
|
+
currentEntity?: EntityShape | null;
|
|
98
|
+
}
|
package/src/index.ts
CHANGED
package/src/traits.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// these as checkboxes and previews the expanded fields, so authors never
|
|
7
7
|
// hand-write `created_date` / `{entity}_id` etc.
|
|
8
8
|
|
|
9
|
-
import type { FieldDef, TraitDef } from "./types";
|
|
9
|
+
import type { FieldDef, TraitDef, TraitsFile } from "./types";
|
|
10
10
|
|
|
11
11
|
export const traits: readonly TraitDef[] = [
|
|
12
12
|
{
|
|
@@ -107,21 +107,61 @@ function templateFields(fields: Record<string, FieldDef>, entity: string): Recor
|
|
|
107
107
|
* for `entityName`, with `{entity}` / `{Entity}` tokens resolved. Returns an
|
|
108
108
|
* empty object for unknown or marker traits. Field order follows include-depth
|
|
109
109
|
* first, then declaration order; later definitions win on key collisions.
|
|
110
|
+
*
|
|
111
|
+
* `localTraits` is a module's own `traits.json`. A module may declare traits of
|
|
112
|
+
* its own, and an entity in it names them exactly like a platform trait — so a
|
|
113
|
+
* reader that knows only this registry sees an entity missing columns that
|
|
114
|
+
* install will give it. Pass the file and those traits resolve; a code declared
|
|
115
|
+
* in both wins locally, mirroring the installer, which overlays the module's
|
|
116
|
+
* traits on the platform ones (`TraitExpanderFactory.ForPackage`).
|
|
110
117
|
*/
|
|
111
|
-
export function expandTrait(
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
export function expandTrait(
|
|
119
|
+
traitCd: string,
|
|
120
|
+
entityName: string,
|
|
121
|
+
localTraits?: TraitsFile,
|
|
122
|
+
): Record<string, FieldDef> {
|
|
123
|
+
return expandTraitInner(traitCd, entityName, localTraits, new Set());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function expandTraitInner(
|
|
127
|
+
traitCd: string,
|
|
128
|
+
entityName: string,
|
|
129
|
+
localTraits: TraitsFile | undefined,
|
|
130
|
+
path: Set<string>,
|
|
131
|
+
): Record<string, FieldDef> {
|
|
132
|
+
// `includes` is author-supplied once local traits are in play, so a cycle is
|
|
133
|
+
// reachable from a module file. Unguarded that is an editor hang, not a bad
|
|
134
|
+
// expansion.
|
|
135
|
+
//
|
|
136
|
+
// The guard is the current path, not every trait already visited: in a
|
|
137
|
+
// diamond — two includes that both reach a third — the third has to expand
|
|
138
|
+
// under each of them, or which definition wins a key collision would depend
|
|
139
|
+
// on which branch got there first.
|
|
140
|
+
if (path.has(traitCd)) return {};
|
|
141
|
+
path.add(traitCd);
|
|
142
|
+
|
|
143
|
+
const trait = localTraits?.[traitCd] ?? byCd.get(traitCd);
|
|
114
144
|
const result: Record<string, FieldDef> = {};
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
if (trait) {
|
|
146
|
+
for (const included of trait.includes ?? []) {
|
|
147
|
+
Object.assign(result, expandTraitInner(included, entityName, localTraits, path));
|
|
148
|
+
}
|
|
149
|
+
Object.assign(result, templateFields(trait.fields ?? {}, entityName));
|
|
117
150
|
}
|
|
118
|
-
|
|
151
|
+
|
|
152
|
+
path.delete(traitCd);
|
|
119
153
|
return result;
|
|
120
154
|
}
|
|
121
155
|
|
|
122
156
|
/** Expand several traits in order into one merged field map. */
|
|
123
|
-
export function expandTraits(
|
|
157
|
+
export function expandTraits(
|
|
158
|
+
traitCds: readonly string[],
|
|
159
|
+
entityName: string,
|
|
160
|
+
localTraits?: TraitsFile,
|
|
161
|
+
): Record<string, FieldDef> {
|
|
124
162
|
const result: Record<string, FieldDef> = {};
|
|
125
|
-
for (const cd of traitCds)
|
|
163
|
+
for (const cd of traitCds) {
|
|
164
|
+
Object.assign(result, expandTraitInner(cd, entityName, localTraits, new Set()));
|
|
165
|
+
}
|
|
126
166
|
return result;
|
|
127
167
|
}
|
package/src/types.ts
CHANGED
|
@@ -179,6 +179,21 @@ export interface TraitDef {
|
|
|
179
179
|
references?: Record<string, unknown>;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* A module's own `traits.json`, keyed by trait code — the key is the `cd`, so a
|
|
184
|
+
* definition here is otherwise a {@link TraitDef} with everything optional
|
|
185
|
+
* (a marker trait declares no fields at all).
|
|
186
|
+
*/
|
|
187
|
+
export interface TraitsFile {
|
|
188
|
+
[traitCd: string]: {
|
|
189
|
+
description?: string;
|
|
190
|
+
includes?: string[];
|
|
191
|
+
fields?: Record<string, FieldDef>;
|
|
192
|
+
references?: Record<string, unknown>;
|
|
193
|
+
constraints?: Record<string, unknown>;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
182
197
|
/** Description of a column kind for friendly pickers. */
|
|
183
198
|
export interface ColumnTypeDef {
|
|
184
199
|
cd: ColumnTypeCd;
|