@dforge-core/metadata 0.0.22 → 0.0.23
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 +134 -0
- package/README.md +45 -0
- package/dist/actions-DvYTrCbP.d.ts +60 -0
- package/dist/dsl/index.d.ts +223 -0
- package/dist/dsl/index.js +1108 -0
- package/dist/dsl/index.js.map +1 -0
- package/dist/index.d.ts +3 -60
- package/package.json +10 -4
- package/schemas/jobs.schema.json +2 -2
- package/src/dsl/builtins.ts +210 -0
- package/src/dsl/check.ts +450 -0
- package/src/dsl/index.ts +36 -0
- package/src/dsl/lexer.ts +364 -0
- package/src/dsl/parse.ts +639 -0
- package/src/dsl/types.ts +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,140 @@ All notable changes to `@dforge-core/metadata` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.23] — 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`@dforge-core/metadata/dsl` — the action-DSL analyzer**, as a second entry point:
|
|
13
|
+
a lexer, a parser and the static rules for `logic/actions/*.dsl`. It was implemented
|
|
14
|
+
twice — once in the language server (token-based, module-aware, drawing squiggles) and
|
|
15
|
+
once in the module validator (regex-based, module-blind, gating a pack) — with only
|
|
16
|
+
three rules in common, so an editor hint and a pre-pack failure disagreed about the
|
|
17
|
+
same script.
|
|
18
|
+
|
|
19
|
+
The rules take what they know about the module through a small injected `DslContext`
|
|
20
|
+
(module code, the action, and the columns of the entity behind the record context)
|
|
21
|
+
rather than a loader, so the same rule serves a language server and a file-walking
|
|
22
|
+
validator. Every context field is optional and every rule that reads one fails open:
|
|
23
|
+
with an empty context the text-only rules still run and the rest stand down silently.
|
|
24
|
+
A false "unknown column" underlines working code and blocks a pack; a missed one costs
|
|
25
|
+
an install round trip.
|
|
26
|
+
|
|
27
|
+
`checkDsl(text, ctx)` returns `DslIssue[]` carrying both byte offsets and a 1-indexed
|
|
28
|
+
line/column, so a range-based host and a `file.dsl:12` reporter each get what they
|
|
29
|
+
need without re-deriving it. `parseDsl`, `tokenize` and the built-in catalog are
|
|
30
|
+
exported too, for completion, hover and go-to-definition. `sendEmail` in that
|
|
31
|
+
catalog is `sendEmail(to, subjectOrTemplate, dataOrBody?)` — three arguments,
|
|
32
|
+
the second choosing template or raw mode, as `ScriptContext.sendEmail` does;
|
|
33
|
+
the fourth argument it used to advertise never existed. `applyProfile` is
|
|
34
|
+
`applyProfile(json, profile)`, taking the inline `{ map, lines? }` object the
|
|
35
|
+
runtime demands — the profile code it used to advertise throws, profile-by-code
|
|
36
|
+
being a later slice. `ocrExtract` states the return type its mode decides: a
|
|
37
|
+
JSON string to `JSON.parse()` without `opts.mode`, a parsed object with it.
|
|
38
|
+
And `download` sets a URL on the action result rather than fetching anything
|
|
39
|
+
into storage, which is what it used to claim.
|
|
40
|
+
|
|
41
|
+
Deliberately **not** in the root barrel: the root export is registries and types that
|
|
42
|
+
the web app and the VS Code webview pull into a browser bundle, and importing it must
|
|
43
|
+
not drag a tokenizer in behind it. The package also declares `sideEffects: false` now.
|
|
44
|
+
|
|
45
|
+
The two existing implementations live in other repos; the duplication ends when
|
|
46
|
+
the language server and the module validator import this entry point, not when
|
|
47
|
+
it ships.
|
|
48
|
+
|
|
49
|
+
Recognising a block the way `ActionDslCompiler.ParseBlocks` recognises one:
|
|
50
|
+
five kinds including `schema:`, anchored at column 0, matched case-insensitively,
|
|
51
|
+
with a leading BOM stripped and `execute:` running to end-of-file. The anchor is
|
|
52
|
+
the load-bearing part — an indented `params:` is an object key inside a block
|
|
53
|
+
body, and treating it as a header fragmented the document and fabricated a
|
|
54
|
+
param list. A header that *is* indented is now reported (`dsl/indented-block-header`),
|
|
55
|
+
since the compiler reads it as body text and runs the block it meant to open
|
|
56
|
+
as empty.
|
|
57
|
+
|
|
58
|
+
`[field]` detection is the compiler's `(?<!\w)\[(\w+)\]`, character for
|
|
59
|
+
character rather than by previous token: the lookbehind does not skip
|
|
60
|
+
whitespace, so a read that opens a line is a read however the line above
|
|
61
|
+
ended. Column and param names match case-insensitively, as `EntityColumnLookup`
|
|
62
|
+
and `ValidateParamUsages` do.
|
|
63
|
+
|
|
64
|
+
A rule is scoped to the block whose compiler actually imposes it. Single-hop
|
|
65
|
+
ref navigation is the DSL's `RxRefNav`, so `canExecute:` — handed to the
|
|
66
|
+
formula engine verbatim, where a chain has no depth limit — is exempt. The
|
|
67
|
+
batch-mode "no current record" rule is `execute:` alone, since
|
|
68
|
+
`onBeforeStart:` is compiled per-record in every mode. And `records[0][field]`
|
|
69
|
+
is one indexed read, matching `RxRecordsFieldRead`, which runs ahead of the
|
|
70
|
+
generic `[field]` rewrite: its column is still checked, but it is not a
|
|
71
|
+
current-record read.
|
|
72
|
+
|
|
73
|
+
What the single-hop rule refuses is a second **bracketed** hop, not a second
|
|
74
|
+
hop: the compiler rewrites the first one and then throws on a surviving `.[`,
|
|
75
|
+
so `[vehicle].plate.length` is one hop followed by ordinary JavaScript on the
|
|
76
|
+
value it returned. Batch mode applies no `[field]` rewrite at all, so
|
|
77
|
+
`var ids = [id]` is the array literal it looks like — the same local-name
|
|
78
|
+
disambiguation the column rule uses. And a param declared inline with its
|
|
79
|
+
header (`params: qty: number required`) is declared: `^params:\s*` swallows
|
|
80
|
+
the newline, making the header's own line the block's first.
|
|
81
|
+
|
|
82
|
+
The column check covers every way the compiler reaches a column, not only the
|
|
83
|
+
bare `[field]`: `old[field]` and a batch loop variable's `x[field]` are fed
|
|
84
|
+
into the same `fieldRefs` set server-side, so a typo in either is the same
|
|
85
|
+
compile error. A template literal is tokenized as prose and holes rather than
|
|
86
|
+
as one opaque token, so `${…}` is analyzed and the text around it is not.
|
|
87
|
+
A param may be named after a block (`execute: number` is a legal `RxParamDecl`
|
|
88
|
+
line) without being read as a header — though its type has to share its line,
|
|
89
|
+
as that single-line regex requires. Nothing inside `params:` is a field read
|
|
90
|
+
at all: `RxKeyValue` takes a bracketed option list, so `options=[cash]` is an
|
|
91
|
+
option list.
|
|
92
|
+
|
|
93
|
+
Case is folded on both sides of the column lookup — the names once per check
|
|
94
|
+
when the lookup can enumerate them, which `Set` and `Map` both can, and the
|
|
95
|
+
reference on the way in — so a mixed-case column name matches whichever case
|
|
96
|
+
the script spells it in. `ColumnLookup` gained an optional `keys()` for that.
|
|
97
|
+
|
|
98
|
+
`dsl/top-level-return` reads function scope rather than brace depth, and is
|
|
99
|
+
an error rather than a warning: both blocks compile to a bare script, and
|
|
100
|
+
Esprima's `ParseScript` refuses a return outside a function wherever it sits,
|
|
101
|
+
so `if (x) { return }` and `if (x) return` fail exactly as a return on its own
|
|
102
|
+
line does. `return` is reserved, so it is a statement unless it is being used
|
|
103
|
+
as a name — a property, an object key, or a method shorthand, told from
|
|
104
|
+
`return (expr)` by the body brace after the parameter list. `function`, function expressions, arrows and method shorthand all
|
|
105
|
+
open a scope where it is legal; an `if` or `for` body does not.
|
|
106
|
+
|
|
107
|
+
A regex literal is one token, so `/return/` and `/[^0-9]/` carry no statement
|
|
108
|
+
and no field read. The `/` is read as division after a value and as a regex
|
|
109
|
+
after an operator or a keyword. `)` and `}` each depend on what they closed —
|
|
110
|
+
a control condition and a statement block leave a statement position, so
|
|
111
|
+
`if (x) /re/.test(s)` and `if (x) {}` then `/re/` are regexes, while
|
|
112
|
+
`(a + b) / 2` and `{ a: 1 }` then `/` divide — and a template chunk on whether
|
|
113
|
+
it stopped at `${` or ran to the backtick.
|
|
114
|
+
|
|
115
|
+
A keyword literal is never a column: in batch `execute:`, which applies no
|
|
116
|
+
`[field]` rewrite at all, `[true]`, `[false]` and `[NULL]` are the array
|
|
117
|
+
elements they look like. In every other block the compiler does rewrite them,
|
|
118
|
+
so the check stands there.
|
|
119
|
+
|
|
120
|
+
`locals` covers function parameters as well as `var`/`let`/`const` and `for`
|
|
121
|
+
heads — declarations, expressions, arrows and method shorthand alike — since a
|
|
122
|
+
parameter binds a name the same way, and `function wrap(value) { return [value] }`
|
|
123
|
+
holds an array literal rather than a field read.
|
|
124
|
+
|
|
125
|
+
Local-name disambiguation is confined to the bare bracket that needs it.
|
|
126
|
+
`var ids = [orderId]` is ambiguous with a field read and only the name tells
|
|
127
|
+
them apart, but `r[stauts]` and `records[0][stauts]` name a column outright —
|
|
128
|
+
the compiler rewrites each to `.get('stauts')` with the literal name — so a
|
|
129
|
+
same-named local no longer hides the typo.
|
|
130
|
+
|
|
131
|
+
Checked against every action in the module tree — 70 actions, each against its
|
|
132
|
+
own trait-expanded entity: no false positives.
|
|
133
|
+
|
|
134
|
+
## [0.0.22] — 2026-08-28
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- `entity.schema.json` — per-constraint validation: `expression` is check-only,
|
|
139
|
+
and a `unique` constraint needs its key. Shipped with the DB-enforced stock
|
|
140
|
+
availability work (`54622c5c`).
|
|
141
|
+
|
|
8
142
|
## [0.0.21] — 2026-08-24
|
|
9
143
|
|
|
10
144
|
### Changed
|
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ pnpm add @dforge-core/metadata
|
|
|
32
32
|
| **Scalar enums** | `FieldTypeCd`, `BaseDatatypeCd`, `ColumnTypeCd`, `FlagCd`, `AlignType` |
|
|
33
33
|
| **Module-file types** | `ManifestDef`, `EntityDef`, `DataViewsFile`, `ReportsFile`, `MenusFile`, `FoldersFile`, `RolesFile`, `SettingsFile`, `JobsFile`, `TriggersFile`, `WebhooksFile`, `PrintTemplatesFile`, `SeedDataFile`, `ActionsFile`, … |
|
|
34
34
|
| **Filter expression** | `Filter`, `FilterCondition`, `FilterGroup`, `SortClause` |
|
|
35
|
+
| **DSL analyzer** | `./dsl` — `checkDsl`, `parseDsl`, `tokenize`, `BUILTINS` (see [Action DSL](#action-dsl)) |
|
|
35
36
|
| **JSON schemas** | `./schemas/<name>.schema.json` (see [Schemas](#json-schemas)) |
|
|
36
37
|
|
|
37
38
|
## Registries & derivation
|
|
@@ -94,6 +95,50 @@ const entity: EntityDef = {
|
|
|
94
95
|
> returned by the API (`metadata.getModel`) is a different contract — see
|
|
95
96
|
> `@dforge/sdk`'s `Model*` types, which reuse the scalar enums from this package.
|
|
96
97
|
|
|
98
|
+
## Action DSL
|
|
99
|
+
|
|
100
|
+
`@dforge-core/metadata/dsl` is a separate entry point holding the lexer, parser
|
|
101
|
+
and static rules for an action body (`logic/actions/*.dsl`). One implementation
|
|
102
|
+
serves both hosts that check these files: the language server, which turns
|
|
103
|
+
issues into squiggles, and the module validator, which gates a pack.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { checkDsl } from "@dforge-core/metadata/dsl";
|
|
107
|
+
|
|
108
|
+
const issues = checkDsl(source, {
|
|
109
|
+
moduleCode: "wms",
|
|
110
|
+
action: { code: "transfer_stock", executionMode: "single" },
|
|
111
|
+
currentEntity: { qualified: "wms.stock_movement", columns: entityColumns },
|
|
112
|
+
});
|
|
113
|
+
// → [{ rule: "dsl/unknown-column", severity: "error", message, start, end, line, column }]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
What a rule knows about the module arrives through that context — a module code,
|
|
117
|
+
the action, and a column lookup (any `Set` or `Map` will do) — rather than
|
|
118
|
+
through a module loader, which is what lets the same rule run in an editor and
|
|
119
|
+
in a file walker. **Every field is optional and every rule that reads one fails
|
|
120
|
+
open**: with an empty context the text-only rules still run and the rest stand
|
|
121
|
+
down silently. A false "unknown column" underlines working code and blocks a
|
|
122
|
+
pack; a missed one costs an install round trip.
|
|
123
|
+
|
|
124
|
+
One obligation comes with `currentEntity`: `columns` must be **trait-expanded**,
|
|
125
|
+
built-in traits and the module's own `traits.json` alike, because the identity
|
|
126
|
+
PK and `[status]` live in traits rather than in `fields`. An unexpanded set
|
|
127
|
+
turns the rule into a wall of false errors. When the entity can't be resolved —
|
|
128
|
+
a bridge module acting on another module's entity — pass `null` rather than the
|
|
129
|
+
fragment you do have.
|
|
130
|
+
|
|
131
|
+
Issues carry both byte offsets and a 1-indexed `line`/`column`, so a
|
|
132
|
+
range-based host and a `file.dsl:12` reporter each get what they need.
|
|
133
|
+
|
|
134
|
+
The blocks, the `[field]` lookbehind and the case rules follow
|
|
135
|
+
`ActionDslCompiler` rather than the prose docs: five block kinds including
|
|
136
|
+
`schema:`, headers anchored at column 0 (an indented one is body text, and is
|
|
137
|
+
reported), and `execute:` running to end-of-file.
|
|
138
|
+
|
|
139
|
+
It is not re-exported from the package root: importing `@dforge-core/metadata`
|
|
140
|
+
in a browser bundle must not pull a tokenizer in behind it.
|
|
141
|
+
|
|
97
142
|
## JSON schemas
|
|
98
143
|
|
|
99
144
|
The 14 canonical schemas are shipped alongside the types and stay byte-identical
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution mode. 'single' (default) and 'each' run the DSL per selected record;
|
|
3
|
+
* 'batch' runs once with all records exposed as `__records`.
|
|
4
|
+
*/
|
|
5
|
+
type ActionExecutionMode = "single" | "each" | "batch";
|
|
6
|
+
/** A single action declaration (value in the `ui/actions.json` map). */
|
|
7
|
+
interface ActionDef {
|
|
8
|
+
/** Human-readable description. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Button label. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Bootstrap icon class (e.g. 'bi-telephone-check'). */
|
|
13
|
+
icon: string;
|
|
14
|
+
/** Entity code this action targets. */
|
|
15
|
+
entityCode: string;
|
|
16
|
+
/** Per-record vs. batch execution. */
|
|
17
|
+
executionMode: ActionExecutionMode;
|
|
18
|
+
/** DSL script name (file under `logic/actions/<script>.dsl`). */
|
|
19
|
+
script: string;
|
|
20
|
+
/**
|
|
21
|
+
* Run the whole selection in one transaction, so a failure on any record
|
|
22
|
+
* rolls back the records already processed. **Defaults to `true`** when
|
|
23
|
+
* omitted (`ActionDef.IsTransacted` in the installer is a non-nullable
|
|
24
|
+
* `bool` initialised to `true`), so omit it only when you want atomicity.
|
|
25
|
+
*
|
|
26
|
+
* `false` opens no transaction, and what follows a failure depends on
|
|
27
|
+
* {@link ActionDef.executionMode}: `single` / `each` report it and continue
|
|
28
|
+
* with the next record, so use that for independent per-record work that
|
|
29
|
+
* should process as many records as pass; `batch` is a single script
|
|
30
|
+
* invocation with no loop to continue, so the run simply ends with its
|
|
31
|
+
* earlier writes committed.
|
|
32
|
+
*
|
|
33
|
+
* On a run queued to the background the **rollback** guarantee is lost
|
|
34
|
+
* outright — the worker opens no transaction, so nothing is ever undone. What
|
|
35
|
+
* remains of the flag there depends on the mode: the worker's `single`/`each`
|
|
36
|
+
* loop still reads it for control flow (stop after the first failed record vs.
|
|
37
|
+
* continue), while its `batch` path never reads it at all — one invocation,
|
|
38
|
+
* which ends on failure under either value.
|
|
39
|
+
*/
|
|
40
|
+
isTransacted?: boolean;
|
|
41
|
+
/** Display order. */
|
|
42
|
+
orderNum?: number;
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated Not read by the installer — use {@link ActionDef.isAsync}.
|
|
45
|
+
* The module manifest model binds `isAsync` only, so this key is silently
|
|
46
|
+
* ignored and the action installs as synchronous.
|
|
47
|
+
*/
|
|
48
|
+
async?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Permit background execution. It does not force it: `action.execute`
|
|
51
|
+
* carries its own `async` argument and the server branches on that, so an
|
|
52
|
+
* action with parameters is offered to the user as both "Run" (inline) and
|
|
53
|
+
* "Run in Background". A parameterless `isAsync` action is queued directly.
|
|
54
|
+
*/
|
|
55
|
+
isAsync?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** A `ui/actions.json` file: action code → definition. */
|
|
58
|
+
type ActionsFile = Record<string, ActionDef>;
|
|
59
|
+
|
|
60
|
+
export type { ActionExecutionMode as A, ActionDef as a, ActionsFile as b };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { A as ActionExecutionMode } from '../actions-DvYTrCbP.js';
|
|
2
|
+
|
|
3
|
+
type TokenKind = "ident" | "number" | "string" | "template" | "regex" | "punct" | "comment";
|
|
4
|
+
interface Token {
|
|
5
|
+
kind: TokenKind;
|
|
6
|
+
/** Source text, verbatim — quotes included for strings. */
|
|
7
|
+
text: string;
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
line: number;
|
|
11
|
+
character: number;
|
|
12
|
+
/** True when a line break sits between this token and the previous one. */
|
|
13
|
+
startsLine: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function tokenize(text: string): Token[];
|
|
16
|
+
/** Strip the surrounding quotes from a string token's text. */
|
|
17
|
+
declare function stringValue(token: Token): string;
|
|
18
|
+
|
|
19
|
+
type BlockKind = "params" | "canExecute" | "schema" | "onBeforeStart" | "execute";
|
|
20
|
+
/** In the order `ActionDslCompiler.ParseBlocks` expects them in a file. */
|
|
21
|
+
declare const BLOCK_KINDS: BlockKind[];
|
|
22
|
+
interface Span {
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
}
|
|
26
|
+
interface DslBlock {
|
|
27
|
+
kind: BlockKind;
|
|
28
|
+
label: Span;
|
|
29
|
+
/** Span of the block body — label end to the next label (or EOF). */
|
|
30
|
+
body: Span;
|
|
31
|
+
}
|
|
32
|
+
interface DslParam {
|
|
33
|
+
name: string;
|
|
34
|
+
nameSpan: Span;
|
|
35
|
+
/** fieldTypeCd, or the referenced entity code when `isRef`. */
|
|
36
|
+
type: string;
|
|
37
|
+
typeSpan: Span;
|
|
38
|
+
isRef: boolean;
|
|
39
|
+
required: boolean;
|
|
40
|
+
label?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A `[field]` read. `navigation` holds the property hops that follow it, each
|
|
44
|
+
* remembering whether it was written `.[target]` or `.target`: the compiler
|
|
45
|
+
* rewrites the FIRST hop either way, and then only a surviving `.[` is the
|
|
46
|
+
* multi-hop compile error. A dotted tail is plain JavaScript on the value the
|
|
47
|
+
* hop returned, so `[customer].[code]`, `[a].b.c` and `[plate].length` all
|
|
48
|
+
* compile.
|
|
49
|
+
*/
|
|
50
|
+
interface FieldRef {
|
|
51
|
+
name: string;
|
|
52
|
+
span: Span;
|
|
53
|
+
/** Span including the brackets. */
|
|
54
|
+
outerSpan: Span;
|
|
55
|
+
navigation: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
span: Span;
|
|
58
|
+
bracketed: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
block: BlockKind | null;
|
|
61
|
+
/**
|
|
62
|
+
* False when the read is bound to a numbered record rather than the current
|
|
63
|
+
* one — `records[0][status]`, which `RxRecordsFieldRead` rewrites ahead of
|
|
64
|
+
* the generic `[field]` pass. The name is still a column of the entity, so
|
|
65
|
+
* the column rules apply; the batch-mode rule does not.
|
|
66
|
+
*/
|
|
67
|
+
isCurrentRecord: boolean;
|
|
68
|
+
/** Index of the token that opens this read — the `[`. */
|
|
69
|
+
startIndex: number;
|
|
70
|
+
/**
|
|
71
|
+
* Index of the last token this read consumes, navigation included. A hop is
|
|
72
|
+
* 2 tokens as `.prop` but 4 as `.[prop]`, so callers that need the token
|
|
73
|
+
* after the read cannot compute it from `navigation.length`.
|
|
74
|
+
*/
|
|
75
|
+
endIndex: number;
|
|
76
|
+
}
|
|
77
|
+
/** `params[x]` / `old[x]` — subscript reads off a DSL global. */
|
|
78
|
+
interface GlobalRef {
|
|
79
|
+
global: "params" | "old" | "records";
|
|
80
|
+
property: string;
|
|
81
|
+
span: Span;
|
|
82
|
+
block: BlockKind | null;
|
|
83
|
+
}
|
|
84
|
+
interface CallRef {
|
|
85
|
+
name: string;
|
|
86
|
+
nameSpan: Span;
|
|
87
|
+
/** First argument when it is a string literal — the entity code, usually. */
|
|
88
|
+
firstStringArg?: {
|
|
89
|
+
value: string;
|
|
90
|
+
span: Span;
|
|
91
|
+
};
|
|
92
|
+
block: BlockKind | null;
|
|
93
|
+
}
|
|
94
|
+
interface DslDocument {
|
|
95
|
+
blocks: DslBlock[];
|
|
96
|
+
params: DslParam[];
|
|
97
|
+
fieldRefs: FieldRef[];
|
|
98
|
+
globalRefs: GlobalRef[];
|
|
99
|
+
calls: CallRef[];
|
|
100
|
+
tokens: Token[];
|
|
101
|
+
/**
|
|
102
|
+
* Every mention of `records`, subscript (`records[x]`) or bare
|
|
103
|
+
* (`for x in records`). `globalRefs` only holds the subscript form, so a
|
|
104
|
+
* rule that needs to point at the batch loop has nothing to anchor to.
|
|
105
|
+
*/
|
|
106
|
+
recordsRefs: Span[];
|
|
107
|
+
/** Names bound by `var`/`let`/`const` or a `for` head, plus declared params. */
|
|
108
|
+
locals: Set<string>;
|
|
109
|
+
/**
|
|
110
|
+
* Loop variables bound by `for x in records {` — `RxForLoop` and nothing
|
|
111
|
+
* else, so an ordinary `for (var x of xs)` is not one. Their `x[field]`
|
|
112
|
+
* subscripts read the entity's columns, which is why they come back as
|
|
113
|
+
* field reads.
|
|
114
|
+
*/
|
|
115
|
+
recordsLoopVars: Set<string>;
|
|
116
|
+
}
|
|
117
|
+
declare function parseDsl(text: string): DslDocument;
|
|
118
|
+
|
|
119
|
+
interface Builtin {
|
|
120
|
+
name: string;
|
|
121
|
+
signature: string;
|
|
122
|
+
returns: string;
|
|
123
|
+
doc: string;
|
|
124
|
+
/**
|
|
125
|
+
* Index of the argument that names an entity, when the built-in takes one.
|
|
126
|
+
* Drives entity-code completion and the qualify-your-codes diagnostic.
|
|
127
|
+
*/
|
|
128
|
+
entityArg?: number;
|
|
129
|
+
}
|
|
130
|
+
declare const BUILTINS: Builtin[];
|
|
131
|
+
declare const BUILTIN_BY_NAME: Map<string, Builtin>;
|
|
132
|
+
/** Bare identifiers the runtime injects — no parentheses. */
|
|
133
|
+
declare const BUILTIN_VALUES: Record<string, string>;
|
|
134
|
+
|
|
135
|
+
type DslSeverity = "error" | "warning" | "info";
|
|
136
|
+
/** One reported problem. `rule` is stable — hosts filter and suppress on it. */
|
|
137
|
+
interface DslIssue {
|
|
138
|
+
/** Stable rule id, namespaced: `dsl/unknown-column`. */
|
|
139
|
+
rule: string;
|
|
140
|
+
severity: DslSeverity;
|
|
141
|
+
message: string;
|
|
142
|
+
/** Byte offsets into the source, for range-based hosts. */
|
|
143
|
+
start: number;
|
|
144
|
+
end: number;
|
|
145
|
+
/** 1-indexed position of `start`, for line-based reporters. */
|
|
146
|
+
line: number;
|
|
147
|
+
column: number;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Column membership test. Structurally satisfied by both `Set<string>` and
|
|
151
|
+
* `Map<string, unknown>`, so a host that already holds a column map passes it
|
|
152
|
+
* straight in rather than copying it into a set on every check.
|
|
153
|
+
*/
|
|
154
|
+
interface ColumnLookup {
|
|
155
|
+
has(name: string): boolean;
|
|
156
|
+
readonly size: number;
|
|
157
|
+
/**
|
|
158
|
+
* The column names, if the lookup can enumerate them — `Set` and `Map` both
|
|
159
|
+
* can. Matching is case-insensitive at the other end (`EntityColumnLookup`
|
|
160
|
+
* builds an OrdinalIgnoreCase set) and a mixed-case column name is legal, so
|
|
161
|
+
* the checker folds case itself when it can read the keys.
|
|
162
|
+
*
|
|
163
|
+
* Without it, a lookup that is not already case-insensitive will report a
|
|
164
|
+
* spelling the server accepts. A host that cannot expose keys should make
|
|
165
|
+
* `has` case-insensitive instead.
|
|
166
|
+
*/
|
|
167
|
+
keys?(): Iterable<string>;
|
|
168
|
+
}
|
|
169
|
+
/** The resolved entity a script's record context refers to. */
|
|
170
|
+
interface EntityShape {
|
|
171
|
+
/** Module-qualified, e.g. `fin.invoice` — used in messages. */
|
|
172
|
+
qualified: string;
|
|
173
|
+
/**
|
|
174
|
+
* Every declared name on the entity, **trait-expanded** — built-in traits
|
|
175
|
+
* and the module's own `traits.json` alike — and including the virtual
|
|
176
|
+
* columns (reference, set, formula), exactly as `EntityColumnLookup` builds
|
|
177
|
+
* the compiler's set.
|
|
178
|
+
*
|
|
179
|
+
* Handing over an unexpanded set is the one mistake that turns this rule
|
|
180
|
+
* into a wall of false errors: the identity PK and `[status]` live in
|
|
181
|
+
* traits, not in `fields`. When in doubt pass `currentEntity: null` — a
|
|
182
|
+
* skipped check costs an install round trip, a wrong one blocks a pack.
|
|
183
|
+
*/
|
|
184
|
+
columns: ColumnLookup;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* What the checker knows about the script's surroundings. Every field is
|
|
188
|
+
* optional and every rule that reads one FAILS OPEN: with an empty context the
|
|
189
|
+
* text-only rules still run and the rest stand down silently.
|
|
190
|
+
*
|
|
191
|
+
* That is deliberate. A false "unknown column" on a correct script is far more
|
|
192
|
+
* damaging than a missed one — an editor would underline working code, and a
|
|
193
|
+
* validator would block a pack — so a rule that cannot resolve what it needs
|
|
194
|
+
* reports nothing.
|
|
195
|
+
*/
|
|
196
|
+
interface DslContext {
|
|
197
|
+
/** Owning module code, for the qualify-your-entity-codes message. */
|
|
198
|
+
moduleCode?: string;
|
|
199
|
+
/** The action this script implements, from `ui/actions.json`. */
|
|
200
|
+
action?: {
|
|
201
|
+
code: string;
|
|
202
|
+
executionMode?: ActionExecutionMode;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Entity behind the record context (`[field]`). `null` or absent when it
|
|
206
|
+
* can't be resolved — a bridge module acting on another module's entity —
|
|
207
|
+
* and then every column rule stands down. A partial set is worse than
|
|
208
|
+
* none: pass `null`, not the fragment. See `EntityShape.columns`.
|
|
209
|
+
*/
|
|
210
|
+
currentEntity?: EntityShape | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check a DSL body. Returns [] for a clean script.
|
|
215
|
+
*
|
|
216
|
+
* Issues come back in source-feature order (field reads, params, execution
|
|
217
|
+
* mode, block spelling, built-ins, top-level returns, inline assignments)
|
|
218
|
+
* rather than sorted by position — a host that wants them in file order can
|
|
219
|
+
* sort on `start`.
|
|
220
|
+
*/
|
|
221
|
+
declare function checkDsl(text: string, ctx?: DslContext): DslIssue[];
|
|
222
|
+
|
|
223
|
+
export { BLOCK_KINDS, BUILTINS, BUILTIN_BY_NAME, BUILTIN_VALUES, type BlockKind, type Builtin, type CallRef, type ColumnLookup, type DslBlock, type DslContext, type DslDocument, type DslIssue, type DslParam, type DslSeverity, type EntityShape, type FieldRef, type GlobalRef, type Span, type Token, type TokenKind, checkDsl, parseDsl, stringValue, tokenize };
|