@dforge-core/dforge-mcp 0.2.22 → 0.2.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 +117 -0
- package/dist/server.js +1638 -292
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +42 -39
- package/resources/schemas/card-layouts.schema.json +141 -0
- package/resources/schemas/data-views.schema.json +2 -14
- package/resources/schemas/entity.schema.json +45 -1
- package/resources/schemas/jobs.schema.json +2 -2
- package/resources/schemas/manifest.schema.json +4 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +1 -8
package/dist/server.js
CHANGED
|
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
|
|
|
31002
31002
|
var fs2 = __toESM(require("fs"));
|
|
31003
31003
|
var path2 = __toESM(require("path"));
|
|
31004
31004
|
|
|
31005
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
31005
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.19/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
31006
31006
|
function buildManifest(opts, moduleId) {
|
|
31007
31007
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31008
31008
|
const manifest = {
|
|
@@ -31283,7 +31283,7 @@ function buildZedTasks() {
|
|
|
31283
31283
|
var fs = __toESM(require("fs"));
|
|
31284
31284
|
var path = __toESM(require("path"));
|
|
31285
31285
|
|
|
31286
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31286
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.27/node_modules/@dforge-core/metadata/dist/index.js
|
|
31287
31287
|
var AggType = {
|
|
31288
31288
|
Sum: "sum",
|
|
31289
31289
|
Count: "count",
|
|
@@ -31435,30 +31435,68 @@ function applyTemplate(value, entity) {
|
|
|
31435
31435
|
const local = entity.split(".").pop() ?? entity;
|
|
31436
31436
|
return value.replace(/\{entity\}/g, local).replace(/\{Entity\}/g, titleCaseEntity(entity));
|
|
31437
31437
|
}
|
|
31438
|
-
function
|
|
31439
|
-
const
|
|
31440
|
-
|
|
31441
|
-
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
31438
|
+
function templateField(def, entity) {
|
|
31439
|
+
const next = { ...def };
|
|
31440
|
+
if (typeof next.description === "string") next.description = applyTemplate(next.description, entity);
|
|
31441
|
+
return next;
|
|
31442
|
+
}
|
|
31443
|
+
function emptyFields() {
|
|
31444
|
+
return /* @__PURE__ */ Object.create(null);
|
|
31445
|
+
}
|
|
31446
|
+
function typeKeyOf(def) {
|
|
31447
|
+
return def.columnType ?? def.dbDatatype ?? def.fieldTypeCd;
|
|
31448
|
+
}
|
|
31449
|
+
function addTraitField(sink, key, def) {
|
|
31450
|
+
const existing = sink.fields[key];
|
|
31451
|
+
if (!Object.hasOwn(sink.fields, key)) {
|
|
31452
|
+
sink.fields[key] = def;
|
|
31453
|
+
sink.origin.set(key, "trait");
|
|
31454
|
+
return;
|
|
31455
|
+
}
|
|
31456
|
+
if (sink.conflicts && !sink.conflicts.some((c) => c.field === key)) {
|
|
31457
|
+
const existingType = typeKeyOf(existing);
|
|
31458
|
+
const traitType = typeKeyOf(def);
|
|
31459
|
+
if (existingType !== traitType) {
|
|
31460
|
+
sink.conflicts.push({
|
|
31461
|
+
field: key,
|
|
31462
|
+
existingFrom: sink.origin.get(key) ?? "trait",
|
|
31463
|
+
existingType,
|
|
31464
|
+
traitType
|
|
31465
|
+
});
|
|
31466
|
+
}
|
|
31445
31467
|
}
|
|
31446
|
-
return out;
|
|
31447
31468
|
}
|
|
31448
|
-
function
|
|
31449
|
-
|
|
31450
|
-
|
|
31451
|
-
const
|
|
31452
|
-
|
|
31453
|
-
|
|
31469
|
+
function expandTraitInner(traitCd, entityName, localTraits, path18, sink) {
|
|
31470
|
+
if (path18.has(traitCd)) return;
|
|
31471
|
+
path18.add(traitCd);
|
|
31472
|
+
const trait = localTraits && Object.hasOwn(localTraits, traitCd) ? localTraits[traitCd] : byCd3.get(traitCd);
|
|
31473
|
+
if (trait) {
|
|
31474
|
+
for (const included of trait.includes ?? []) {
|
|
31475
|
+
expandTraitInner(included, entityName, localTraits, path18, sink);
|
|
31476
|
+
}
|
|
31477
|
+
for (const [name, def] of Object.entries(trait.fields ?? {})) {
|
|
31478
|
+
addTraitField(sink, applyTemplate(name, entityName), templateField(def, entityName));
|
|
31479
|
+
}
|
|
31454
31480
|
}
|
|
31455
|
-
|
|
31456
|
-
return result;
|
|
31481
|
+
path18.delete(traitCd);
|
|
31457
31482
|
}
|
|
31458
|
-
function expandTraits(traitCds, entityName) {
|
|
31459
|
-
const
|
|
31460
|
-
for (const cd of traitCds)
|
|
31461
|
-
|
|
31483
|
+
function expandTraits(traitCds, entityName, localTraits) {
|
|
31484
|
+
const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map() };
|
|
31485
|
+
for (const cd of traitCds) {
|
|
31486
|
+
expandTraitInner(cd, entityName, localTraits, /* @__PURE__ */ new Set(), sink);
|
|
31487
|
+
}
|
|
31488
|
+
return sink.fields;
|
|
31489
|
+
}
|
|
31490
|
+
function traitFieldConflicts(traitCds, entityName, opts = {}) {
|
|
31491
|
+
const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map(), conflicts: [] };
|
|
31492
|
+
for (const [key, def] of Object.entries(opts.fields ?? {})) {
|
|
31493
|
+
sink.fields[key] = def;
|
|
31494
|
+
sink.origin.set(key, "field");
|
|
31495
|
+
}
|
|
31496
|
+
for (const cd of traitCds) {
|
|
31497
|
+
expandTraitInner(cd, entityName, opts.localTraits, /* @__PURE__ */ new Set(), sink);
|
|
31498
|
+
}
|
|
31499
|
+
return sink.conflicts;
|
|
31462
31500
|
}
|
|
31463
31501
|
var NUMERIC_TOTAL = 18;
|
|
31464
31502
|
var TEXT_BACKED = /* @__PURE__ */ new Set(["textarea", "markdown", "richtext", "code"]);
|
|
@@ -31715,14 +31753,107 @@ var traitsInput = external_exports.array(external_exports.string()).default(["id
|
|
|
31715
31753
|
}).describe(
|
|
31716
31754
|
"Entity trait codes \u2014 identity, audit, audit-full, soft-delete, sorting, postable, accumulation, ledger, period. 'identity' makes the PK '{entity}_id'. Traits expand into columns server-side at install; list only the codes."
|
|
31717
31755
|
);
|
|
31718
|
-
function unknownTraits(traitCodes) {
|
|
31719
|
-
return traitCodes.filter(
|
|
31756
|
+
function unknownTraits(traitCodes, localTraits) {
|
|
31757
|
+
return traitCodes.filter(
|
|
31758
|
+
(cd) => !TRAIT_CODE_SET.has(cd) && localTraits?.[cd] === void 0
|
|
31759
|
+
);
|
|
31760
|
+
}
|
|
31761
|
+
var TRAIT_DEF_KEYS = ["cd", "description", "includes", "fields", "references", "constraints"];
|
|
31762
|
+
var TRAIT_DEF_KEY_SET = new Set(TRAIT_DEF_KEYS);
|
|
31763
|
+
var isPlainObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
31764
|
+
function traitsShapeError(parsed) {
|
|
31765
|
+
if (!isPlainObject3(parsed)) {
|
|
31766
|
+
const got = Array.isArray(parsed) ? "an array" : parsed === null ? "null" : `a ${typeof parsed}`;
|
|
31767
|
+
return `not a traits file: expected an object keyed by trait code, got ${got}.`;
|
|
31768
|
+
}
|
|
31769
|
+
for (const [cd, def] of Object.entries(parsed)) {
|
|
31770
|
+
if (!isPlainObject3(def)) {
|
|
31771
|
+
const got = Array.isArray(def) ? "an array" : def === null ? "null" : `a ${typeof def}`;
|
|
31772
|
+
return `not a traits file: trait '${cd}' is ${got}, not a definition object.`;
|
|
31773
|
+
}
|
|
31774
|
+
const unexpected = Object.keys(def).filter((k) => !TRAIT_DEF_KEY_SET.has(k));
|
|
31775
|
+
if (unexpected.length > 0) {
|
|
31776
|
+
if (cd === "traits" && Object.keys(parsed).length === 1) {
|
|
31777
|
+
return `not a traits file: the top level is keyed by trait code, so a "traits" wrapper registers one trait called 'traits' and hides the ${unexpected.length} real one(s) (${unexpected.join(", ")}). Remove the wrapper and put the definitions at the top level.`;
|
|
31778
|
+
}
|
|
31779
|
+
return `not a traits file: trait '${cd}' has unexpected key(s): ${unexpected.join(", ")}. A trait definition carries ${TRAIT_DEF_KEYS.join(", ")}.`;
|
|
31780
|
+
}
|
|
31781
|
+
if (def.fields !== void 0 && !isPlainObject3(def.fields)) {
|
|
31782
|
+
return `not a traits file: trait '${cd}' has a 'fields' that is not an object keyed by column code.`;
|
|
31783
|
+
}
|
|
31784
|
+
const includes = def.includes;
|
|
31785
|
+
if (includes !== void 0 && !(Array.isArray(includes) && includes.every((i) => typeof i === "string"))) {
|
|
31786
|
+
return `not a traits file: trait '${cd}' has an 'includes' that is not an array of trait codes.`;
|
|
31787
|
+
}
|
|
31788
|
+
}
|
|
31789
|
+
return void 0;
|
|
31790
|
+
}
|
|
31791
|
+
function readLocalTraits(moduleRoot) {
|
|
31792
|
+
const file2 = path.join(moduleRoot, "traits.json");
|
|
31793
|
+
if (!fs.existsSync(file2)) return {};
|
|
31794
|
+
let parsed;
|
|
31795
|
+
try {
|
|
31796
|
+
parsed = JSON.parse(fs.readFileSync(file2, "utf8"));
|
|
31797
|
+
} catch (ex) {
|
|
31798
|
+
return { error: `invalid JSON: ${ex.message}.` };
|
|
31799
|
+
}
|
|
31800
|
+
const shape = traitsShapeError(parsed);
|
|
31801
|
+
if (shape) return { error: shape };
|
|
31802
|
+
return { traits: parsed };
|
|
31803
|
+
}
|
|
31804
|
+
function expandedEntity(entity, entityName, localTraits) {
|
|
31805
|
+
const fields = entity.fields ?? {};
|
|
31806
|
+
const traitCodes = entity.traits ?? [];
|
|
31807
|
+
const traitFields = expandTraits(traitCodes, entityName, localTraits);
|
|
31808
|
+
return {
|
|
31809
|
+
columns: /* @__PURE__ */ new Set([...Object.keys(fields), ...Object.keys(traitFields)]),
|
|
31810
|
+
fieldDefs: { ...traitFields, ...fields },
|
|
31811
|
+
// The expansion above is total — a trait field the entity already has
|
|
31812
|
+
// under a different type is merged away here, while the installer keeps
|
|
31813
|
+
// the first and refuses the entity outright. Ask for those collisions
|
|
31814
|
+
// separately, or they are invisible to every caller.
|
|
31815
|
+
conflicts: traitFieldConflicts(traitCodes, entityName, {
|
|
31816
|
+
localTraits,
|
|
31817
|
+
fields
|
|
31818
|
+
})
|
|
31819
|
+
};
|
|
31820
|
+
}
|
|
31821
|
+
function describeTraitConflict(c) {
|
|
31822
|
+
const kept = c.existingFrom === "field" ? "an authored field" : "another trait's field";
|
|
31823
|
+
return `field '${c.field}' is contributed by a trait but already exists as ${kept} with a different type (${c.existingType ?? "untyped"} vs ${c.traitType ?? "untyped"}). The installer keeps the first and refuses the entity \u2014 rename the column, or drop the trait that brings it.`;
|
|
31824
|
+
}
|
|
31825
|
+
function localEntityCode(code2, moduleCode) {
|
|
31826
|
+
if (!code2) return void 0;
|
|
31827
|
+
const dot = code2.indexOf(".");
|
|
31828
|
+
if (dot < 0) return code2;
|
|
31829
|
+
return code2.slice(0, dot) === moduleCode ? code2.slice(dot + 1) : void 0;
|
|
31830
|
+
}
|
|
31831
|
+
function entityRecordContext(paths, manifest, entityCode, localTraits) {
|
|
31832
|
+
const local = localEntityCode(entityCode, manifest.code);
|
|
31833
|
+
if (!local) return void 0;
|
|
31834
|
+
const relPath = (manifest.entities ?? {})[local];
|
|
31835
|
+
if (!relPath) return void 0;
|
|
31836
|
+
const abs = path.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
31837
|
+
if (!fs.existsSync(abs)) return void 0;
|
|
31838
|
+
let entity;
|
|
31839
|
+
try {
|
|
31840
|
+
entity = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
31841
|
+
} catch {
|
|
31842
|
+
return void 0;
|
|
31843
|
+
}
|
|
31844
|
+
const traitCodes = entity.traits ?? [];
|
|
31845
|
+
if (unknownTraits(traitCodes, localTraits).length > 0) return void 0;
|
|
31846
|
+
return {
|
|
31847
|
+
qualified: `${manifest.code}.${local}`,
|
|
31848
|
+
columns: expandedEntity(entity, local, localTraits).columns
|
|
31849
|
+
};
|
|
31720
31850
|
}
|
|
31721
|
-
function assertKnownTraits(traitCodes, entityName) {
|
|
31722
|
-
const bad = unknownTraits(traitCodes);
|
|
31851
|
+
function assertKnownTraits(traitCodes, entityName, localTraits) {
|
|
31852
|
+
const bad = unknownTraits(traitCodes, localTraits);
|
|
31723
31853
|
if (bad.length === 0) return;
|
|
31854
|
+
const local = Object.keys(localTraits ?? {});
|
|
31724
31855
|
throw new Error(
|
|
31725
|
-
`Entity '${entityName}' declares unknown trait(s): ${bad.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}. An unrecognized trait is silently ignored when columns are expanded, so its columns would quietly go missing rather than fail loudly. (See dforge://reference/traits.)
|
|
31856
|
+
`Entity '${entityName}' declares unknown trait(s): ${bad.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}` + (local.length > 0 ? `, plus this module's own traits.json: ${local.join(", ")}` : "") + ". An unrecognized trait is silently ignored when columns are expanded, so its columns would quietly go missing rather than fail loudly. (See dforge://reference/traits.)"
|
|
31726
31857
|
);
|
|
31727
31858
|
}
|
|
31728
31859
|
function withTraits(entity, traitCodes) {
|
|
@@ -34317,307 +34448,1447 @@ function entityDelete(args) {
|
|
|
34317
34448
|
// src/tools/action-add.ts
|
|
34318
34449
|
var path9 = __toESM(require("path"));
|
|
34319
34450
|
|
|
34320
|
-
//
|
|
34321
|
-
var
|
|
34322
|
-
var
|
|
34323
|
-
"
|
|
34324
|
-
"
|
|
34325
|
-
"
|
|
34326
|
-
"
|
|
34327
|
-
"
|
|
34328
|
-
"
|
|
34329
|
-
"
|
|
34330
|
-
"
|
|
34331
|
-
"
|
|
34332
|
-
"
|
|
34333
|
-
"
|
|
34334
|
-
"
|
|
34335
|
-
"
|
|
34336
|
-
"
|
|
34337
|
-
"
|
|
34338
|
-
|
|
34339
|
-
|
|
34340
|
-
"getFileUrl",
|
|
34341
|
-
"getRecord",
|
|
34342
|
-
"getRecordOrNull",
|
|
34343
|
-
"getSecret",
|
|
34344
|
-
"getSetting",
|
|
34345
|
-
"info",
|
|
34346
|
-
"insert",
|
|
34347
|
-
"nextNumber",
|
|
34348
|
-
"notify",
|
|
34349
|
-
"now",
|
|
34350
|
-
"ocrExtract",
|
|
34351
|
-
"preloadRef",
|
|
34352
|
-
"query",
|
|
34353
|
-
"select",
|
|
34354
|
-
"sendEmail",
|
|
34355
|
-
"tryParseJson",
|
|
34356
|
-
"update",
|
|
34357
|
-
"warn"
|
|
34358
|
-
]);
|
|
34359
|
-
var JS_RESERVED = /* @__PURE__ */ new Set([
|
|
34451
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.27/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34452
|
+
var PUNCT3 = ["===", "!=="];
|
|
34453
|
+
var PUNCT2 = [
|
|
34454
|
+
"==",
|
|
34455
|
+
"!=",
|
|
34456
|
+
"<=",
|
|
34457
|
+
">=",
|
|
34458
|
+
"&&",
|
|
34459
|
+
"||",
|
|
34460
|
+
"++",
|
|
34461
|
+
"--",
|
|
34462
|
+
"+=",
|
|
34463
|
+
"-=",
|
|
34464
|
+
"*=",
|
|
34465
|
+
"/=",
|
|
34466
|
+
"%=",
|
|
34467
|
+
"${",
|
|
34468
|
+
"=>"
|
|
34469
|
+
];
|
|
34470
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
34360
34471
|
"if",
|
|
34361
|
-
"for",
|
|
34362
34472
|
"while",
|
|
34473
|
+
"for",
|
|
34363
34474
|
"switch",
|
|
34364
34475
|
"catch",
|
|
34365
|
-
"
|
|
34366
|
-
"typeof",
|
|
34367
|
-
"function",
|
|
34368
|
-
"do",
|
|
34369
|
-
"else",
|
|
34370
|
-
"in",
|
|
34371
|
-
"of",
|
|
34372
|
-
"new",
|
|
34373
|
-
"delete",
|
|
34374
|
-
"void",
|
|
34375
|
-
"throw",
|
|
34376
|
-
"try",
|
|
34377
|
-
"parseInt",
|
|
34378
|
-
"parseFloat",
|
|
34379
|
-
"isNaN",
|
|
34380
|
-
"isFinite",
|
|
34381
|
-
"String",
|
|
34382
|
-
"Number",
|
|
34383
|
-
"Boolean",
|
|
34384
|
-
"Array",
|
|
34385
|
-
"Object",
|
|
34386
|
-
"Date",
|
|
34387
|
-
"Math",
|
|
34388
|
-
"JSON",
|
|
34389
|
-
"RegExp",
|
|
34390
|
-
"Error"
|
|
34476
|
+
"with"
|
|
34391
34477
|
]);
|
|
34392
|
-
|
|
34393
|
-
|
|
34394
|
-
TODAY: "now()",
|
|
34395
|
-
NOW: "now()",
|
|
34396
|
-
CURRENT_USER_ID: "currentUserId()"
|
|
34397
|
-
};
|
|
34398
|
-
function blank(src) {
|
|
34399
|
-
let out = "";
|
|
34478
|
+
function tokenize(text) {
|
|
34479
|
+
const tokens = [];
|
|
34400
34480
|
let i = 0;
|
|
34401
|
-
|
|
34402
|
-
|
|
34403
|
-
|
|
34404
|
-
|
|
34405
|
-
|
|
34406
|
-
|
|
34407
|
-
|
|
34408
|
-
|
|
34409
|
-
|
|
34481
|
+
let line = 0;
|
|
34482
|
+
let lineStart = 0;
|
|
34483
|
+
let pendingLineBreak = true;
|
|
34484
|
+
const frames = [];
|
|
34485
|
+
const parenIsControl = [];
|
|
34486
|
+
let closedControlParen = false;
|
|
34487
|
+
const braceIsBlock = [];
|
|
34488
|
+
let closedBlockBrace = false;
|
|
34489
|
+
const push = (kind, start, end) => {
|
|
34490
|
+
tokens.push({
|
|
34491
|
+
kind,
|
|
34492
|
+
text: text.slice(start, end),
|
|
34493
|
+
start,
|
|
34494
|
+
end,
|
|
34495
|
+
line,
|
|
34496
|
+
character: start - lineStart,
|
|
34497
|
+
startsLine: pendingLineBreak
|
|
34498
|
+
});
|
|
34499
|
+
pendingLineBreak = false;
|
|
34500
|
+
};
|
|
34501
|
+
while (i < text.length) {
|
|
34502
|
+
const frame = frames[frames.length - 1];
|
|
34503
|
+
if (frame?.kind === "template") {
|
|
34504
|
+
const startLine = line;
|
|
34505
|
+
const startLineStart = lineStart;
|
|
34506
|
+
while (i < text.length) {
|
|
34507
|
+
const ch = text[i];
|
|
34508
|
+
if (ch === "\\") {
|
|
34410
34509
|
i += 2;
|
|
34411
34510
|
continue;
|
|
34412
34511
|
}
|
|
34413
|
-
|
|
34512
|
+
if (ch === "`" || ch === "$" && text[i + 1] === "{") break;
|
|
34513
|
+
if (ch === "\n") {
|
|
34514
|
+
line++;
|
|
34515
|
+
lineStart = i + 1;
|
|
34516
|
+
}
|
|
34414
34517
|
i++;
|
|
34415
34518
|
}
|
|
34416
|
-
if (i
|
|
34417
|
-
|
|
34519
|
+
if (text[i] === "`") {
|
|
34520
|
+
i++;
|
|
34521
|
+
frames.pop();
|
|
34522
|
+
} else if (i < text.length) {
|
|
34523
|
+
i += 2;
|
|
34524
|
+
frames.push({ kind: "interp", braces: 0 });
|
|
34525
|
+
} else {
|
|
34526
|
+
frames.pop();
|
|
34527
|
+
}
|
|
34528
|
+
const savedLine = line;
|
|
34529
|
+
const savedLineStart = lineStart;
|
|
34530
|
+
line = startLine;
|
|
34531
|
+
lineStart = startLineStart;
|
|
34532
|
+
push("template", frame.chunkStart, i);
|
|
34533
|
+
line = savedLine;
|
|
34534
|
+
lineStart = savedLineStart;
|
|
34535
|
+
continue;
|
|
34536
|
+
}
|
|
34537
|
+
const c = text[i];
|
|
34538
|
+
if (c === "\uFEFF" && i === 0) {
|
|
34539
|
+
i++;
|
|
34540
|
+
lineStart = i;
|
|
34541
|
+
continue;
|
|
34542
|
+
}
|
|
34543
|
+
if (c === "\n") {
|
|
34544
|
+
i++;
|
|
34545
|
+
line++;
|
|
34546
|
+
lineStart = i;
|
|
34547
|
+
pendingLineBreak = true;
|
|
34548
|
+
continue;
|
|
34549
|
+
}
|
|
34550
|
+
if (c === " " || c === " " || c === "\r") {
|
|
34551
|
+
i++;
|
|
34552
|
+
continue;
|
|
34553
|
+
}
|
|
34554
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
34555
|
+
const start = i;
|
|
34556
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
34557
|
+
push("comment", start, i);
|
|
34558
|
+
continue;
|
|
34559
|
+
}
|
|
34560
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
34561
|
+
const start = i;
|
|
34562
|
+
const startLine = line;
|
|
34563
|
+
const startLineStart = lineStart;
|
|
34564
|
+
i += 2;
|
|
34565
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) {
|
|
34566
|
+
if (text[i] === "\n") {
|
|
34567
|
+
line++;
|
|
34568
|
+
lineStart = i + 1;
|
|
34569
|
+
}
|
|
34418
34570
|
i++;
|
|
34419
34571
|
}
|
|
34572
|
+
i = Math.min(i + 2, text.length);
|
|
34573
|
+
const savedLine = line;
|
|
34574
|
+
const savedLineStart = lineStart;
|
|
34575
|
+
line = startLine;
|
|
34576
|
+
lineStart = startLineStart;
|
|
34577
|
+
push("comment", start, i);
|
|
34578
|
+
line = savedLine;
|
|
34579
|
+
lineStart = savedLineStart;
|
|
34420
34580
|
continue;
|
|
34421
34581
|
}
|
|
34422
|
-
if (c === "
|
|
34423
|
-
|
|
34424
|
-
|
|
34582
|
+
if (c === "'" || c === '"') {
|
|
34583
|
+
const start = i;
|
|
34584
|
+
const quote = c;
|
|
34585
|
+
i++;
|
|
34586
|
+
while (i < text.length && text[i] !== quote && text[i] !== "\n") {
|
|
34587
|
+
if (text[i] === "\\") i++;
|
|
34425
34588
|
i++;
|
|
34426
34589
|
}
|
|
34590
|
+
if (text[i] === quote) i++;
|
|
34591
|
+
push("string", start, i);
|
|
34427
34592
|
continue;
|
|
34428
34593
|
}
|
|
34429
|
-
|
|
34594
|
+
if (c === "`") {
|
|
34595
|
+
frames.push({ kind: "template", chunkStart: i });
|
|
34596
|
+
i++;
|
|
34597
|
+
continue;
|
|
34598
|
+
}
|
|
34599
|
+
if (c === "/" && regexAllowedAfter(
|
|
34600
|
+
tokens[tokens.length - 1],
|
|
34601
|
+
closedControlParen,
|
|
34602
|
+
closedBlockBrace
|
|
34603
|
+
)) {
|
|
34604
|
+
const end = scanRegex(text, i);
|
|
34605
|
+
if (end > 0) {
|
|
34606
|
+
push("regex", i, end);
|
|
34607
|
+
i = end;
|
|
34608
|
+
continue;
|
|
34609
|
+
}
|
|
34610
|
+
}
|
|
34611
|
+
if (/[0-9]/.test(c)) {
|
|
34612
|
+
const start = i;
|
|
34613
|
+
while (i < text.length && /[0-9._eE]/.test(text[i])) i++;
|
|
34614
|
+
push("number", start, i);
|
|
34615
|
+
continue;
|
|
34616
|
+
}
|
|
34617
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
34618
|
+
const start = i;
|
|
34619
|
+
while (i < text.length && /[A-Za-z0-9_$]/.test(text[i])) i++;
|
|
34620
|
+
push("ident", start, i);
|
|
34621
|
+
continue;
|
|
34622
|
+
}
|
|
34623
|
+
if (c === "(") {
|
|
34624
|
+
const prev = tokens[tokens.length - 1];
|
|
34625
|
+
parenIsControl.push(
|
|
34626
|
+
prev?.kind === "ident" && CONTROL_KEYWORDS.has(prev.text)
|
|
34627
|
+
);
|
|
34628
|
+
} else if (c === ")") {
|
|
34629
|
+
closedControlParen = parenIsControl.pop() ?? false;
|
|
34630
|
+
}
|
|
34631
|
+
if (frame?.kind === "interp") {
|
|
34632
|
+
if (c === "}" && frame.braces === 0) {
|
|
34633
|
+
frames.pop();
|
|
34634
|
+
frames[frames.length - 1].chunkStart = i;
|
|
34635
|
+
i++;
|
|
34636
|
+
continue;
|
|
34637
|
+
}
|
|
34638
|
+
if (c === "{") frame.braces++;
|
|
34639
|
+
else if (c === "}") frame.braces--;
|
|
34640
|
+
}
|
|
34641
|
+
if (c === "{") {
|
|
34642
|
+
braceIsBlock.push(isBlockBrace(tokens[tokens.length - 1]));
|
|
34643
|
+
} else if (c === "}") {
|
|
34644
|
+
closedBlockBrace = braceIsBlock.pop() ?? false;
|
|
34645
|
+
}
|
|
34646
|
+
const three = text.slice(i, i + 3);
|
|
34647
|
+
if (PUNCT3.includes(three)) {
|
|
34648
|
+
push("punct", i, i + 3);
|
|
34649
|
+
i += 3;
|
|
34650
|
+
continue;
|
|
34651
|
+
}
|
|
34652
|
+
const two = text.slice(i, i + 2);
|
|
34653
|
+
if (PUNCT2.includes(two)) {
|
|
34654
|
+
push("punct", i, i + 2);
|
|
34655
|
+
i += 2;
|
|
34656
|
+
continue;
|
|
34657
|
+
}
|
|
34658
|
+
push("punct", i, i + 1);
|
|
34430
34659
|
i++;
|
|
34431
34660
|
}
|
|
34432
|
-
return
|
|
34433
|
-
}
|
|
34434
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34437
|
-
if (
|
|
34438
|
-
|
|
34661
|
+
return tokens;
|
|
34662
|
+
}
|
|
34663
|
+
function regexAllowedAfter(prev, closedControlParen, closedBlockBrace) {
|
|
34664
|
+
if (!prev) return true;
|
|
34665
|
+
if (prev.kind === "number" || prev.kind === "string") return false;
|
|
34666
|
+
if (prev.kind === "template") return prev.text.endsWith("${");
|
|
34667
|
+
if (prev.kind === "ident") return REGEX_PRECEDING_KEYWORDS.has(prev.text);
|
|
34668
|
+
if (prev.kind === "regex") return false;
|
|
34669
|
+
if (prev.kind === "comment") return true;
|
|
34670
|
+
if (prev.text === ")") return closedControlParen;
|
|
34671
|
+
if (prev.text === "}") return closedBlockBrace;
|
|
34672
|
+
return !REGEX_BLOCKING_PUNCT.has(prev.text);
|
|
34673
|
+
}
|
|
34674
|
+
function isBlockBrace(prev) {
|
|
34675
|
+
if (!prev) return true;
|
|
34676
|
+
if (prev.kind === "ident") return BLOCK_PRECEDING_KEYWORDS.has(prev.text);
|
|
34677
|
+
if (prev.kind !== "punct") return false;
|
|
34678
|
+
return prev.text === ")" || prev.text === "}" || prev.text === "{" || prev.text === ";";
|
|
34679
|
+
}
|
|
34680
|
+
var BLOCK_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set(["else", "do", "try", "finally"]);
|
|
34681
|
+
var REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
|
|
34682
|
+
"return",
|
|
34683
|
+
"typeof",
|
|
34684
|
+
"instanceof",
|
|
34685
|
+
"in",
|
|
34686
|
+
"of",
|
|
34687
|
+
"new",
|
|
34688
|
+
"delete",
|
|
34689
|
+
"void",
|
|
34690
|
+
"case",
|
|
34691
|
+
"do",
|
|
34692
|
+
"else",
|
|
34693
|
+
"throw",
|
|
34694
|
+
"yield",
|
|
34695
|
+
"await"
|
|
34696
|
+
]);
|
|
34697
|
+
var REGEX_BLOCKING_PUNCT = /* @__PURE__ */ new Set(["]", "++", "--"]);
|
|
34698
|
+
function scanRegex(text, start) {
|
|
34439
34699
|
let i = start + 1;
|
|
34440
|
-
|
|
34441
|
-
|
|
34700
|
+
let inClass = false;
|
|
34701
|
+
if (text[i] === "/" || text[i] === "*") return -1;
|
|
34702
|
+
for (; i < text.length; i++) {
|
|
34703
|
+
const c = text[i];
|
|
34442
34704
|
if (c === "\\") {
|
|
34443
|
-
|
|
34705
|
+
i++;
|
|
34706
|
+
continue;
|
|
34707
|
+
}
|
|
34708
|
+
if (c === "\n") return -1;
|
|
34709
|
+
if (c === "[") inClass = true;
|
|
34710
|
+
else if (c === "]") inClass = false;
|
|
34711
|
+
else if (c === "/" && !inClass) {
|
|
34712
|
+
i++;
|
|
34713
|
+
while (i < text.length && /[a-z]/.test(text[i])) i++;
|
|
34714
|
+
return i;
|
|
34715
|
+
}
|
|
34716
|
+
}
|
|
34717
|
+
return -1;
|
|
34718
|
+
}
|
|
34719
|
+
function stringValue(token) {
|
|
34720
|
+
if (token.kind !== "string") return token.text;
|
|
34721
|
+
const body = token.text.slice(1, token.text.endsWith(token.text[0]) && token.text.length > 1 ? -1 : void 0);
|
|
34722
|
+
return unescape2(body);
|
|
34723
|
+
}
|
|
34724
|
+
function templateChunkValue(token) {
|
|
34725
|
+
if (token.kind !== "template") return token.text;
|
|
34726
|
+
const body = token.text.slice(1);
|
|
34727
|
+
if (body.endsWith("${")) return unescape2(body.slice(0, -2));
|
|
34728
|
+
if (body.endsWith("`")) return unescape2(body.slice(0, -1));
|
|
34729
|
+
return unescape2(body);
|
|
34730
|
+
}
|
|
34731
|
+
function unescape2(body) {
|
|
34732
|
+
return body.replace(/\\(.)/g, (_, ch) => {
|
|
34733
|
+
switch (ch) {
|
|
34734
|
+
case "n":
|
|
34735
|
+
return "\n";
|
|
34736
|
+
case "t":
|
|
34737
|
+
return " ";
|
|
34738
|
+
case "r":
|
|
34739
|
+
return "\r";
|
|
34740
|
+
default:
|
|
34741
|
+
return ch;
|
|
34742
|
+
}
|
|
34743
|
+
});
|
|
34744
|
+
}
|
|
34745
|
+
var BLOCK_KINDS = [
|
|
34746
|
+
"params",
|
|
34747
|
+
"canExecute",
|
|
34748
|
+
"schema",
|
|
34749
|
+
"onBeforeStart",
|
|
34750
|
+
"execute"
|
|
34751
|
+
];
|
|
34752
|
+
var BLOCK_BY_LOWER = new Map(BLOCK_KINDS.map((k) => [k.toLowerCase(), k]));
|
|
34753
|
+
var GLOBALS = /* @__PURE__ */ new Set(["params", "old", "records"]);
|
|
34754
|
+
function parseDsl(text) {
|
|
34755
|
+
const all = tokenize(text);
|
|
34756
|
+
const tokens = all.filter((t) => t.kind !== "comment");
|
|
34757
|
+
const blocks = findBlocks(text, tokens);
|
|
34758
|
+
const blockAt = (offset) => blocks.find((b) => offset >= b.body.start && offset < b.body.end)?.kind ?? null;
|
|
34759
|
+
const params = parseParams(tokens, blocks);
|
|
34760
|
+
const fieldRefs = [];
|
|
34761
|
+
const globalRefs = [];
|
|
34762
|
+
const calls = [];
|
|
34763
|
+
const recordsRefs = [];
|
|
34764
|
+
const locals = collectLocals(tokens, params);
|
|
34765
|
+
const recordsLoopVars = collectRecordsLoopVars(tokens);
|
|
34766
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
34767
|
+
const t = tokens[i];
|
|
34768
|
+
if (t.kind === "punct" && t.text === "[" && isFieldAccessStart(text, t.start)) {
|
|
34769
|
+
const name = tokens[i + 1];
|
|
34770
|
+
const close = tokens[i + 2];
|
|
34771
|
+
if (name?.kind === "ident" && close?.text === "]") {
|
|
34772
|
+
const ref = {
|
|
34773
|
+
name: name.text,
|
|
34774
|
+
span: { start: name.start, end: name.end },
|
|
34775
|
+
outerSpan: { start: t.start, end: close.end },
|
|
34776
|
+
navigation: [],
|
|
34777
|
+
block: blockAt(t.start),
|
|
34778
|
+
isCurrentRecord: true,
|
|
34779
|
+
startIndex: i,
|
|
34780
|
+
endIndex: i + 2
|
|
34781
|
+
};
|
|
34782
|
+
ref.endIndex = collectNavigation(tokens, i + 2, ref);
|
|
34783
|
+
i = ref.endIndex;
|
|
34784
|
+
fieldRefs.push(ref);
|
|
34785
|
+
continue;
|
|
34786
|
+
}
|
|
34787
|
+
}
|
|
34788
|
+
if (t.kind === "ident" && t.text === "records" && tokens[i - 1]?.text !== ".") {
|
|
34789
|
+
recordsRefs.push({ start: t.start, end: t.end });
|
|
34790
|
+
const field = indexedRecordField(tokens, i);
|
|
34791
|
+
if (field) {
|
|
34792
|
+
fieldRefs.push({
|
|
34793
|
+
name: field.name.text,
|
|
34794
|
+
span: { start: field.name.start, end: field.name.end },
|
|
34795
|
+
outerSpan: { start: field.open.start, end: field.close.end },
|
|
34796
|
+
navigation: [],
|
|
34797
|
+
block: blockAt(t.start),
|
|
34798
|
+
isCurrentRecord: false,
|
|
34799
|
+
startIndex: field.openIndex,
|
|
34800
|
+
endIndex: field.openIndex + 2
|
|
34801
|
+
});
|
|
34802
|
+
i = field.openIndex + 2;
|
|
34803
|
+
continue;
|
|
34804
|
+
}
|
|
34805
|
+
}
|
|
34806
|
+
if (t.kind === "ident" && recordsLoopVars.has(t.text)) {
|
|
34807
|
+
const open = tokens[i + 1];
|
|
34808
|
+
const name = tokens[i + 2];
|
|
34809
|
+
const close = tokens[i + 3];
|
|
34810
|
+
if (open?.text === "[" && name?.kind === "ident" && close?.text === "]") {
|
|
34811
|
+
fieldRefs.push({
|
|
34812
|
+
name: name.text,
|
|
34813
|
+
span: { start: name.start, end: name.end },
|
|
34814
|
+
outerSpan: { start: open.start, end: close.end },
|
|
34815
|
+
navigation: [],
|
|
34816
|
+
block: blockAt(t.start),
|
|
34817
|
+
isCurrentRecord: false,
|
|
34818
|
+
startIndex: i + 1,
|
|
34819
|
+
endIndex: i + 3
|
|
34820
|
+
});
|
|
34821
|
+
i += 3;
|
|
34822
|
+
continue;
|
|
34823
|
+
}
|
|
34824
|
+
}
|
|
34825
|
+
if (t.kind === "ident" && GLOBALS.has(t.text)) {
|
|
34826
|
+
const open = tokens[i + 1];
|
|
34827
|
+
const prop = tokens[i + 2];
|
|
34828
|
+
const close = tokens[i + 3];
|
|
34829
|
+
if (open?.text === "[" && prop?.kind === "ident" && close?.text === "]") {
|
|
34830
|
+
globalRefs.push({
|
|
34831
|
+
global: t.text,
|
|
34832
|
+
property: prop.text,
|
|
34833
|
+
span: { start: prop.start, end: prop.end },
|
|
34834
|
+
block: blockAt(t.start)
|
|
34835
|
+
});
|
|
34836
|
+
i += 3;
|
|
34837
|
+
continue;
|
|
34838
|
+
}
|
|
34839
|
+
}
|
|
34840
|
+
if (t.kind === "ident" && tokens[i + 1]?.text === "(") {
|
|
34841
|
+
const prev = tokens[i - 1];
|
|
34842
|
+
if (prev?.text === ".") continue;
|
|
34843
|
+
const call = {
|
|
34844
|
+
name: t.text,
|
|
34845
|
+
nameSpan: { start: t.start, end: t.end },
|
|
34846
|
+
block: blockAt(t.start)
|
|
34847
|
+
};
|
|
34848
|
+
const firstArg = tokens[i + 2];
|
|
34849
|
+
if (firstArg?.kind === "string") {
|
|
34850
|
+
call.firstStringArg = {
|
|
34851
|
+
value: stringValue(firstArg),
|
|
34852
|
+
span: { start: firstArg.start, end: firstArg.end },
|
|
34853
|
+
interpolated: false,
|
|
34854
|
+
endIndex: i + 2
|
|
34855
|
+
};
|
|
34856
|
+
} else if (firstArg?.kind === "template" && firstArg.text.startsWith("`")) {
|
|
34857
|
+
call.firstStringArg = templateArg(tokens, i + 2);
|
|
34858
|
+
}
|
|
34859
|
+
calls.push(call);
|
|
34860
|
+
}
|
|
34861
|
+
}
|
|
34862
|
+
return {
|
|
34863
|
+
blocks,
|
|
34864
|
+
params,
|
|
34865
|
+
fieldRefs,
|
|
34866
|
+
globalRefs,
|
|
34867
|
+
calls,
|
|
34868
|
+
tokens,
|
|
34869
|
+
recordsRefs,
|
|
34870
|
+
locals,
|
|
34871
|
+
recordsLoopVars
|
|
34872
|
+
};
|
|
34873
|
+
}
|
|
34874
|
+
function isFieldAccessStart(text, start) {
|
|
34875
|
+
return start === 0 || !/\w/.test(text[start - 1]);
|
|
34876
|
+
}
|
|
34877
|
+
function templateArg(tokens, start) {
|
|
34878
|
+
const parts = [];
|
|
34879
|
+
let depth = 0;
|
|
34880
|
+
let interpolated = false;
|
|
34881
|
+
let last = tokens[start];
|
|
34882
|
+
let endIndex = start;
|
|
34883
|
+
for (let j = start; j < tokens.length; j++) {
|
|
34884
|
+
const t = tokens[j];
|
|
34885
|
+
if (t.kind !== "template") continue;
|
|
34886
|
+
const opens = t.text.startsWith("`");
|
|
34887
|
+
if (opens) depth++;
|
|
34888
|
+
if (depth === 1) {
|
|
34889
|
+
if (!opens) parts.push(" ");
|
|
34890
|
+
parts.push(templateChunkValue(t));
|
|
34891
|
+
last = t;
|
|
34892
|
+
endIndex = j;
|
|
34893
|
+
}
|
|
34894
|
+
if (t.text.endsWith("${")) {
|
|
34895
|
+
if (depth === 1) interpolated = true;
|
|
34896
|
+
continue;
|
|
34897
|
+
}
|
|
34898
|
+
depth--;
|
|
34899
|
+
if (depth === 0) break;
|
|
34900
|
+
}
|
|
34901
|
+
return {
|
|
34902
|
+
value: parts.join(""),
|
|
34903
|
+
span: { start: tokens[start].start, end: last.end },
|
|
34904
|
+
interpolated,
|
|
34905
|
+
endIndex
|
|
34906
|
+
};
|
|
34907
|
+
}
|
|
34908
|
+
function indexedRecordField(tokens, i) {
|
|
34909
|
+
const index = tokens[i + 2];
|
|
34910
|
+
if (tokens[i + 1]?.text !== "[") return null;
|
|
34911
|
+
if (index?.kind !== "number" || !/^\d+$/.test(index.text)) return null;
|
|
34912
|
+
if (tokens[i + 3]?.text !== "]") return null;
|
|
34913
|
+
const open = tokens[i + 4];
|
|
34914
|
+
const name = tokens[i + 5];
|
|
34915
|
+
const close = tokens[i + 6];
|
|
34916
|
+
if (open?.text !== "[" || name?.kind !== "ident" || close?.text !== "]")
|
|
34917
|
+
return null;
|
|
34918
|
+
return { name, open, close, openIndex: i + 4 };
|
|
34919
|
+
}
|
|
34920
|
+
function collectNavigation(tokens, closeIdx, ref) {
|
|
34921
|
+
let i = closeIdx;
|
|
34922
|
+
for (; ; ) {
|
|
34923
|
+
const dot = tokens[i + 1];
|
|
34924
|
+
if (dot?.text !== ".") return i;
|
|
34925
|
+
const next = tokens[i + 2];
|
|
34926
|
+
if (next?.kind === "ident") {
|
|
34927
|
+
if (tokens[i + 3]?.text === "(") return i;
|
|
34928
|
+
ref.navigation.push({
|
|
34929
|
+
name: next.text,
|
|
34930
|
+
span: { start: next.start, end: next.end },
|
|
34931
|
+
bracketed: false
|
|
34932
|
+
});
|
|
34444
34933
|
i += 2;
|
|
34445
34934
|
continue;
|
|
34446
34935
|
}
|
|
34447
|
-
if (
|
|
34448
|
-
|
|
34449
|
-
|
|
34936
|
+
if (next?.text === "[") {
|
|
34937
|
+
const name = tokens[i + 3];
|
|
34938
|
+
const close = tokens[i + 4];
|
|
34939
|
+
if (name?.kind === "ident" && close?.text === "]") {
|
|
34940
|
+
ref.navigation.push({
|
|
34941
|
+
name: name.text,
|
|
34942
|
+
span: { start: name.start, end: name.end },
|
|
34943
|
+
bracketed: true
|
|
34944
|
+
});
|
|
34945
|
+
i += 4;
|
|
34946
|
+
continue;
|
|
34947
|
+
}
|
|
34948
|
+
}
|
|
34949
|
+
return i;
|
|
34450
34950
|
}
|
|
34451
|
-
return null;
|
|
34452
34951
|
}
|
|
34453
|
-
function
|
|
34454
|
-
const
|
|
34455
|
-
|
|
34456
|
-
|
|
34457
|
-
const
|
|
34458
|
-
|
|
34459
|
-
if (
|
|
34460
|
-
|
|
34952
|
+
function collectRecordsLoopVars(tokens) {
|
|
34953
|
+
const vars = /* @__PURE__ */ new Set();
|
|
34954
|
+
for (let i = 0; i < tokens.length - 4; i++) {
|
|
34955
|
+
if (tokens[i].text !== "for" || tokens[i].kind !== "ident") continue;
|
|
34956
|
+
const name = tokens[i + 1];
|
|
34957
|
+
if (name.kind !== "ident") continue;
|
|
34958
|
+
if (tokens[i + 2].text !== "in" || tokens[i + 3].text !== "records") continue;
|
|
34959
|
+
if (tokens[i + 4].text !== "{") continue;
|
|
34960
|
+
vars.add(name.text);
|
|
34461
34961
|
}
|
|
34462
|
-
return
|
|
34962
|
+
return vars;
|
|
34463
34963
|
}
|
|
34464
|
-
|
|
34465
|
-
|
|
34466
|
-
const
|
|
34467
|
-
|
|
34468
|
-
for (let i = 0; i <
|
|
34469
|
-
const
|
|
34470
|
-
if (
|
|
34471
|
-
|
|
34964
|
+
var DECLARATORS = /* @__PURE__ */ new Set(["var", "let", "const"]);
|
|
34965
|
+
function collectLocals(tokens, params) {
|
|
34966
|
+
const locals = new Set(params.map((p) => p.name));
|
|
34967
|
+
collectFunctionParams(tokens, locals);
|
|
34968
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
34969
|
+
const t = tokens[i];
|
|
34970
|
+
if (t.kind !== "ident") continue;
|
|
34971
|
+
if (DECLARATORS.has(t.text)) {
|
|
34972
|
+
collectDeclarations(tokens, i + 1, locals);
|
|
34973
|
+
continue;
|
|
34974
|
+
}
|
|
34975
|
+
if (t.text === "for") {
|
|
34976
|
+
let j = i + 1;
|
|
34977
|
+
if (tokens[j]?.text === "(") j++;
|
|
34978
|
+
if (tokens[j]?.kind === "ident" && DECLARATORS.has(tokens[j].text)) j++;
|
|
34979
|
+
const name = tokens[j];
|
|
34980
|
+
if (name?.kind === "ident" && !DECLARATORS.has(name.text)) locals.add(name.text);
|
|
34981
|
+
}
|
|
34472
34982
|
}
|
|
34473
|
-
|
|
34474
|
-
|
|
34983
|
+
return locals;
|
|
34984
|
+
}
|
|
34985
|
+
function collectFunctionParams(tokens, locals) {
|
|
34986
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
34987
|
+
const t = tokens[i];
|
|
34988
|
+
if (t.kind === "ident" && t.text === "function") {
|
|
34989
|
+
let j = i + 1;
|
|
34990
|
+
if (tokens[j]?.kind === "ident") j++;
|
|
34991
|
+
if (tokens[j]?.text === "(") collectParenIdents(tokens, j, locals);
|
|
34992
|
+
continue;
|
|
34993
|
+
}
|
|
34994
|
+
if (t.text === "=>") {
|
|
34995
|
+
const prev = tokens[i - 1];
|
|
34996
|
+
if (prev?.kind === "ident") locals.add(prev.text);
|
|
34997
|
+
else if (prev?.text === ")") {
|
|
34998
|
+
const open = matchingOpenParen(tokens, i - 1);
|
|
34999
|
+
if (open >= 0) collectParenIdents(tokens, open, locals);
|
|
35000
|
+
}
|
|
35001
|
+
continue;
|
|
35002
|
+
}
|
|
35003
|
+
if (t.kind === "ident" && !CONTROL_KEYWORDS.has(t.text) && tokens[i + 1]?.text === "(") {
|
|
35004
|
+
const close = matchingCloseParen(tokens, i + 1);
|
|
35005
|
+
if (close >= 0 && tokens[close + 1]?.text === "{")
|
|
35006
|
+
collectParenIdents(tokens, i + 1, locals);
|
|
35007
|
+
}
|
|
35008
|
+
}
|
|
35009
|
+
}
|
|
35010
|
+
function collectParenIdents(tokens, openIndex, locals) {
|
|
35011
|
+
let depth = 0;
|
|
35012
|
+
for (let i = openIndex; i < tokens.length; i++) {
|
|
35013
|
+
const t = tokens[i];
|
|
35014
|
+
if (t.text === "(") depth++;
|
|
35015
|
+
else if (t.text === ")") {
|
|
35016
|
+
depth--;
|
|
35017
|
+
if (depth === 0) return;
|
|
35018
|
+
} else if (t.kind === "ident") locals.add(t.text);
|
|
35019
|
+
}
|
|
35020
|
+
}
|
|
35021
|
+
function matchingCloseParen(tokens, openIndex) {
|
|
35022
|
+
let depth = 0;
|
|
35023
|
+
for (let i = openIndex; i < tokens.length; i++) {
|
|
35024
|
+
if (tokens[i].text === "(") depth++;
|
|
35025
|
+
else if (tokens[i].text === ")" && --depth === 0) return i;
|
|
35026
|
+
}
|
|
35027
|
+
return -1;
|
|
35028
|
+
}
|
|
35029
|
+
function matchingOpenParen(tokens, closeIndex) {
|
|
35030
|
+
let depth = 0;
|
|
35031
|
+
for (let i = closeIndex; i >= 0; i--) {
|
|
35032
|
+
if (tokens[i].text === ")") depth++;
|
|
35033
|
+
else if (tokens[i].text === "(" && --depth === 0) return i;
|
|
35034
|
+
}
|
|
35035
|
+
return -1;
|
|
35036
|
+
}
|
|
35037
|
+
function collectDeclarations(tokens, start, locals) {
|
|
35038
|
+
let j = start;
|
|
35039
|
+
for (; ; ) {
|
|
35040
|
+
const head = tokens[j];
|
|
35041
|
+
if (!head) return;
|
|
35042
|
+
if (head.text === "{" || head.text === "[") {
|
|
35043
|
+
j = collectPattern(tokens, j, locals);
|
|
35044
|
+
} else if (head.kind === "ident" && !DECLARATORS.has(head.text)) {
|
|
35045
|
+
locals.add(head.text);
|
|
35046
|
+
j++;
|
|
35047
|
+
} else return;
|
|
35048
|
+
let depth = 0;
|
|
35049
|
+
for (; j < tokens.length; j++) {
|
|
35050
|
+
const n = tokens[j];
|
|
35051
|
+
if (n.text === "(" || n.text === "[" || n.text === "{") depth++;
|
|
35052
|
+
else if (n.text === ")" || n.text === "]" || n.text === "}") depth--;
|
|
35053
|
+
if (depth < 0) return;
|
|
35054
|
+
if (depth > 0) continue;
|
|
35055
|
+
if (n.text === ";") return;
|
|
35056
|
+
if (n.text === ",") break;
|
|
35057
|
+
if (n.startsLine) return;
|
|
35058
|
+
}
|
|
35059
|
+
if (j >= tokens.length) return;
|
|
35060
|
+
j++;
|
|
35061
|
+
}
|
|
35062
|
+
}
|
|
35063
|
+
function collectPattern(tokens, start, locals) {
|
|
35064
|
+
let depth = 0;
|
|
35065
|
+
let i = start;
|
|
35066
|
+
for (; i < tokens.length; i++) {
|
|
35067
|
+
const t = tokens[i];
|
|
35068
|
+
if (t.text === "{" || t.text === "[") depth++;
|
|
35069
|
+
else if (t.text === "}" || t.text === "]") {
|
|
35070
|
+
depth--;
|
|
35071
|
+
if (depth === 0) return i + 1;
|
|
35072
|
+
} else if (t.kind === "ident") locals.add(t.text);
|
|
35073
|
+
}
|
|
35074
|
+
return i;
|
|
35075
|
+
}
|
|
35076
|
+
function blockLabelAt(tokens, i) {
|
|
35077
|
+
const t = tokens[i];
|
|
35078
|
+
if (!t || t.kind !== "ident" || !t.startsLine) return null;
|
|
35079
|
+
const colon = tokens[i + 1];
|
|
35080
|
+
if (colon?.text !== ":" || colon.start !== t.end) return null;
|
|
35081
|
+
const kind = BLOCK_BY_LOWER.get(t.text.toLowerCase());
|
|
35082
|
+
return kind ? { kind, span: { start: t.start, end: colon.end } } : null;
|
|
35083
|
+
}
|
|
35084
|
+
function findBlocks(text, tokens) {
|
|
35085
|
+
const labels = [];
|
|
35086
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
35087
|
+
const t = tokens[i];
|
|
35088
|
+
if (t.character !== 0) continue;
|
|
35089
|
+
const label = blockLabelAt(tokens, i);
|
|
35090
|
+
if (!label) continue;
|
|
35091
|
+
labels.push(label);
|
|
35092
|
+
if (label.kind === "execute") break;
|
|
35093
|
+
}
|
|
35094
|
+
return labels.map((l, idx) => ({
|
|
35095
|
+
kind: l.kind,
|
|
35096
|
+
label: l.span,
|
|
35097
|
+
body: {
|
|
35098
|
+
start: l.span.end,
|
|
35099
|
+
end: labels[idx + 1]?.span.start ?? text.length
|
|
35100
|
+
}
|
|
35101
|
+
}));
|
|
35102
|
+
}
|
|
35103
|
+
function parseParams(tokens, blocks) {
|
|
35104
|
+
const block = blocks.find((b) => b.kind === "params");
|
|
35105
|
+
if (!block) return [];
|
|
35106
|
+
const inBlock = tokens.filter(
|
|
35107
|
+
(t) => t.start >= block.body.start && t.start < block.body.end
|
|
35108
|
+
);
|
|
35109
|
+
const params = [];
|
|
35110
|
+
for (let i = 0; i < inBlock.length; i++) {
|
|
35111
|
+
const name = inBlock[i];
|
|
35112
|
+
if (!name.startsLine && i !== 0 || name.kind !== "ident") continue;
|
|
35113
|
+
if (inBlock[i + 1]?.text !== ":") continue;
|
|
35114
|
+
let j = i + 2;
|
|
35115
|
+
let isRef = false;
|
|
35116
|
+
if (inBlock[j]?.kind === "ident" && inBlock[j].text === "ref") {
|
|
35117
|
+
isRef = true;
|
|
35118
|
+
j++;
|
|
35119
|
+
}
|
|
35120
|
+
const type = inBlock[j];
|
|
35121
|
+
if (!type || type.kind !== "ident") continue;
|
|
35122
|
+
if (type.line !== name.line) continue;
|
|
35123
|
+
let typeEnd = type.end;
|
|
35124
|
+
let typeText = type.text;
|
|
35125
|
+
if (isRef) {
|
|
35126
|
+
while (inBlock[j + 1] && (inBlock[j + 1].text === "-" || inBlock[j + 1].text === ".") && inBlock[j + 2]?.kind === "ident" && inBlock[j + 1].start === typeEnd && inBlock[j + 2].start === inBlock[j + 1].end) {
|
|
35127
|
+
typeText += inBlock[j + 1].text + inBlock[j + 2].text;
|
|
35128
|
+
typeEnd = inBlock[j + 2].end;
|
|
35129
|
+
j += 2;
|
|
35130
|
+
}
|
|
35131
|
+
}
|
|
35132
|
+
let required2 = false;
|
|
35133
|
+
let label;
|
|
35134
|
+
for (let k = j + 1; k < inBlock.length && !inBlock[k].startsLine; k++) {
|
|
35135
|
+
const t = inBlock[k];
|
|
35136
|
+
if (t.kind === "ident" && t.text === "required") required2 = true;
|
|
35137
|
+
if (t.kind === "string" && label === void 0) label = stringValue(t);
|
|
35138
|
+
}
|
|
35139
|
+
params.push({
|
|
35140
|
+
name: name.text,
|
|
35141
|
+
nameSpan: { start: name.start, end: name.end },
|
|
35142
|
+
type: typeText,
|
|
35143
|
+
typeSpan: { start: type.start, end: typeEnd },
|
|
35144
|
+
isRef,
|
|
35145
|
+
required: required2,
|
|
35146
|
+
label
|
|
35147
|
+
});
|
|
34475
35148
|
}
|
|
34476
|
-
return
|
|
35149
|
+
return params;
|
|
34477
35150
|
}
|
|
34478
|
-
|
|
34479
|
-
|
|
34480
|
-
|
|
34481
|
-
|
|
34482
|
-
|
|
34483
|
-
|
|
34484
|
-
|
|
34485
|
-
|
|
34486
|
-
|
|
34487
|
-
|
|
34488
|
-
|
|
35151
|
+
var BUILTINS = [
|
|
35152
|
+
// Data operations
|
|
35153
|
+
{
|
|
35154
|
+
name: "insert",
|
|
35155
|
+
signature: "insert(entity, fields)",
|
|
35156
|
+
returns: "object",
|
|
35157
|
+
entityArg: 0,
|
|
35158
|
+
doc: "Insert a new record. Returns the full row including auto-filled columns (PK, audit, number sequences)."
|
|
35159
|
+
},
|
|
35160
|
+
{
|
|
35161
|
+
name: "select",
|
|
35162
|
+
signature: "select(entity, opts?)",
|
|
35163
|
+
returns: "object[]",
|
|
35164
|
+
entityArg: 0,
|
|
35165
|
+
doc: "Structured multi-row read. `opts` = `{columns, filter, orderBy, limit, offset}`. Prefer over `query()` for row-shaped reads."
|
|
35166
|
+
},
|
|
35167
|
+
{
|
|
35168
|
+
name: "update",
|
|
35169
|
+
signature: "update(entity, key, fields)",
|
|
35170
|
+
returns: "number",
|
|
35171
|
+
entityArg: 0,
|
|
35172
|
+
doc: "Update the row(s) matched by `key`; returns rows affected. A value may be `{ inc: n }` to add to the column's current value \u2014 use that for every counter, balance and quantity."
|
|
35173
|
+
},
|
|
35174
|
+
{
|
|
35175
|
+
name: "delete",
|
|
35176
|
+
signature: "delete(entity, key)",
|
|
35177
|
+
returns: "number",
|
|
35178
|
+
entityArg: 0,
|
|
35179
|
+
doc: "Delete the row(s) matched by `key`; returns rows affected."
|
|
35180
|
+
},
|
|
35181
|
+
{
|
|
35182
|
+
name: "query",
|
|
35183
|
+
signature: "query(sql, args?)",
|
|
35184
|
+
returns: "object[]",
|
|
35185
|
+
doc: "Execute parameterized SQL with `@argName` placeholders and schema-qualified table names. Escape hatch \u2014 prefer `select()` / `update()` / `delete()` when the structured shape fits."
|
|
35186
|
+
},
|
|
35187
|
+
{
|
|
35188
|
+
name: "getRecord",
|
|
35189
|
+
signature: "getRecord(entity, key)",
|
|
35190
|
+
returns: "object",
|
|
35191
|
+
entityArg: 0,
|
|
35192
|
+
doc: "Fetch a single record by key. **Throws** a localized not-found error when absent."
|
|
35193
|
+
},
|
|
35194
|
+
{
|
|
35195
|
+
name: "getRecordOrNull",
|
|
35196
|
+
signature: "getRecordOrNull(entity, key)",
|
|
35197
|
+
returns: "object | null",
|
|
35198
|
+
entityArg: 0,
|
|
35199
|
+
doc: "Like `getRecord`, but returns `null` instead of throwing \u2014 for an expected absence, guarded with `if (rec == null)`."
|
|
35200
|
+
},
|
|
35201
|
+
{
|
|
35202
|
+
name: "preloadRef",
|
|
35203
|
+
signature: "preloadRef(fkColumn)",
|
|
35204
|
+
returns: "object",
|
|
35205
|
+
doc: "Load the referenced record for a FK column. Read its fields with `ref.get('field')`."
|
|
35206
|
+
},
|
|
35207
|
+
// Messaging
|
|
35208
|
+
{
|
|
35209
|
+
name: "error",
|
|
35210
|
+
signature: "error(message)",
|
|
35211
|
+
returns: "never",
|
|
35212
|
+
doc: "Abort the action and show an error. Rolls back all changes."
|
|
35213
|
+
},
|
|
35214
|
+
{ name: "warn", signature: "warn(message)", returns: "void", doc: "Show a warning and continue." },
|
|
35215
|
+
{
|
|
35216
|
+
name: "info",
|
|
35217
|
+
signature: "info(message)",
|
|
35218
|
+
returns: "void",
|
|
35219
|
+
doc: "Show an informational message \u2014 a receipt for work the action did, not a way to publish a computed value."
|
|
35220
|
+
},
|
|
35221
|
+
{
|
|
35222
|
+
name: "exit",
|
|
35223
|
+
signature: "exit(message?, level?)",
|
|
35224
|
+
returns: "never",
|
|
35225
|
+
doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default) or `'warn'`. This is the DSL's early return \u2014 the block compiles to a bare script, so a `return` outside a function is a syntax error wherever it sits."
|
|
35226
|
+
},
|
|
35227
|
+
{
|
|
35228
|
+
name: "notify",
|
|
35229
|
+
signature: "notify(userId, message)",
|
|
35230
|
+
returns: "void",
|
|
35231
|
+
doc: "Send an in-app notification to a specific user."
|
|
35232
|
+
},
|
|
35233
|
+
{
|
|
35234
|
+
name: "sendEmail",
|
|
35235
|
+
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?)",
|
|
35236
|
+
returns: "void",
|
|
35237
|
+
doc: "Queue an outbound email. Three arguments, and the **second** picks the mode: `sendEmail(to, 'order.shipped', { ref: [code] })` sends a template, `sendEmail(to, 'Your order shipped', '<p>\u2026</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code \u2014 so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces. There is no fourth argument."
|
|
35238
|
+
},
|
|
35239
|
+
// External calls
|
|
35240
|
+
{
|
|
35241
|
+
name: "callApi",
|
|
35242
|
+
signature: "callApi(url, method, headers?, body?)",
|
|
35243
|
+
returns: "string",
|
|
35244
|
+
doc: "HTTP request to an external API; returns the body as a string. Max 10 calls per script, 120s timeout, 5MB response."
|
|
35245
|
+
},
|
|
35246
|
+
{
|
|
35247
|
+
name: "callService",
|
|
35248
|
+
signature: "callService(name, args)",
|
|
35249
|
+
returns: "any",
|
|
35250
|
+
doc: "Invoke a registered host service."
|
|
35251
|
+
},
|
|
35252
|
+
{
|
|
35253
|
+
name: "callProc",
|
|
35254
|
+
signature: "callProc(procName, args)",
|
|
35255
|
+
returns: "any",
|
|
35256
|
+
doc: "Call a stored procedure with named parameters."
|
|
35257
|
+
},
|
|
35258
|
+
// Files, settings, extraction
|
|
35259
|
+
{
|
|
35260
|
+
name: "getFileBase64",
|
|
35261
|
+
signature: "getFileBase64(fileField)",
|
|
35262
|
+
returns: "string",
|
|
35263
|
+
doc: "Read a file from storage as base64. Use with `callApi()` to send files to external APIs. Max 10 MB."
|
|
35264
|
+
},
|
|
35265
|
+
{
|
|
35266
|
+
name: "getFileUrl",
|
|
35267
|
+
signature: "getFileUrl(fileField)",
|
|
35268
|
+
returns: "string",
|
|
35269
|
+
doc: "Temporary signed download URL (relative path) for a file field \u2014 for emails and notifications, not for `callApi()`."
|
|
35270
|
+
},
|
|
35271
|
+
{ name: "getFileInfo", signature: "getFileInfo(fileField)", returns: "object | null", doc: "File metadata for a file field." },
|
|
35272
|
+
{
|
|
35273
|
+
name: "download",
|
|
35274
|
+
signature: "download(url, fileName?)",
|
|
35275
|
+
returns: "void",
|
|
35276
|
+
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing \u2014 the URL must already serve the bytes, e.g. the `/api/temp-file/{token}` a service handed back. To put a remote file INTO storage, fetch it with `callApi()` and write it to a file column."
|
|
35277
|
+
},
|
|
35278
|
+
{
|
|
35279
|
+
name: "getSetting",
|
|
35280
|
+
signature: "getSetting(settingCd)",
|
|
35281
|
+
returns: "any",
|
|
35282
|
+
doc: "Read a module setting, resolved through the folder chain (current folder \u2192 parents \u2192 module default)."
|
|
35283
|
+
},
|
|
35284
|
+
{
|
|
35285
|
+
name: "getSecret",
|
|
35286
|
+
signature: "getSecret(secretCd)",
|
|
35287
|
+
returns: "string",
|
|
35288
|
+
doc: "Retrieve a decrypted secret (API key, token). Managed in the admin UI."
|
|
35289
|
+
},
|
|
35290
|
+
{
|
|
35291
|
+
name: "ocrExtract",
|
|
35292
|
+
signature: "ocrExtract(fileField, endpointBaseUrl, schema?, opts?)",
|
|
35293
|
+
returns: "string (v1) | object (v2)",
|
|
35294
|
+
doc: "Run OCR extraction against a document. **The return type follows the mode, and only `opts.mode` selects it** \u2014 a third argument alone never does. Without `opts.mode` (v1) the third argument is forwarded as `hints` and the result is the raw bundle as a JSON **string**: `JSON.parse()` it, or every property read is `undefined`. With `{ mode: 'extract' }` the third argument is a schema object and the result is already parsed \u2014 `{ fields, confidence?, unmatched, raw_text, meta }`. `{ mode: 'summary' }` needs no schema and returns `{ summary, raw_text, meta }` parsed. `{ profile: 'module_cd.profile_cd' }` uses a registered extraction profile, implies mode `extract`, and cannot be combined with an inline schema. The OCR host must be on `Dsl:Limits:AllowedHosts`."
|
|
35295
|
+
},
|
|
35296
|
+
{
|
|
35297
|
+
name: "detectDocument",
|
|
35298
|
+
signature: "detectDocument(rawText)",
|
|
35299
|
+
returns: "object | null",
|
|
35300
|
+
doc: "Score text against reachable extraction profiles; returns the best match `{ profile, docType, score }` or `null`."
|
|
35301
|
+
},
|
|
35302
|
+
{
|
|
35303
|
+
name: "applyProfile",
|
|
35304
|
+
signature: "applyProfile(json, profile)",
|
|
35305
|
+
returns: "object",
|
|
35306
|
+
doc: "Map already-extracted JSON to target fields and return the mapped object \u2014 it never writes, so the `insert()` / `update()` stays yours. `json` is the parsed extractor output (`JSON.parse(ocrExtract(...))`). `profile` must be an **inline object**, `{ map: { target: { from, transform?, pick? } }, lines?: { from, map, key? } }` \u2014 passing a profile code throws, as profile-by-code is a later slice. `from` is a path into the extraction; `transform` is one of `firstValid:<basis>`, `lookupRef:<entity.matchField>>returnField>`, `matchCatalog:<same>`, `parseMoney` or `parseDate`. Header targets come back at the top level and the 1:N block under its `key` (default `lines`); a missing source path omits the target. Not the same thing as `ocrExtract`'s registered `opts.profile` code."
|
|
35307
|
+
},
|
|
35308
|
+
// Utilities
|
|
35309
|
+
{ name: "now", signature: "now()", returns: "datetime", doc: "Current date/time. Lowercase \u2014 this is the execute-block date function." },
|
|
35310
|
+
{ name: "addDays", signature: "addDays(date, n)", returns: "date", doc: "Add `n` days to a date." },
|
|
35311
|
+
{ name: "addMinutes", signature: "addMinutes(date, n)", returns: "datetime", doc: "Add `n` minutes." },
|
|
35312
|
+
{ name: "addSeconds", signature: "addSeconds(date, n)", returns: "datetime", doc: "Add `n` seconds." },
|
|
35313
|
+
{ name: "currentUserId", signature: "currentUserId()", returns: "long", doc: "The acting user's tenant user ID." },
|
|
35314
|
+
{ name: "flush", signature: "flush()", returns: "void", doc: "Flush pending writes so a following read sees them." },
|
|
35315
|
+
{ name: "tryParseJson", signature: "tryParseJson(value)", returns: "any | null", doc: "Parse JSON, returning `null` on failure instead of throwing." },
|
|
35316
|
+
{
|
|
35317
|
+
name: "nextNumber",
|
|
35318
|
+
signature: "nextNumber(entity)",
|
|
35319
|
+
returns: "string",
|
|
35320
|
+
entityArg: 0,
|
|
35321
|
+
doc: "Generate the next value from a number sequence. Usually unnecessary \u2014 the platform auto-fills on `insert()`."
|
|
35322
|
+
},
|
|
35323
|
+
{
|
|
35324
|
+
name: "entityLink",
|
|
35325
|
+
signature: "entityLink(entityCd, record, description?)",
|
|
35326
|
+
returns: "link",
|
|
35327
|
+
entityArg: 0,
|
|
35328
|
+
doc: "Build a clickable link to a record, for an `entitylink` column. Always pass a **qualified** code \u2014 `entity_cd` is only unique per module."
|
|
35329
|
+
}
|
|
35330
|
+
];
|
|
35331
|
+
var BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
|
|
35332
|
+
function checkDsl(text, ctx = {}) {
|
|
35333
|
+
const parsed = parseDsl(text);
|
|
35334
|
+
const out = [];
|
|
35335
|
+
const positionAt = lineIndexer(text);
|
|
35336
|
+
const add = (span, message, severity, rule) => {
|
|
35337
|
+
const { line, column } = positionAt(span.start);
|
|
35338
|
+
out.push({
|
|
35339
|
+
rule,
|
|
35340
|
+
severity,
|
|
35341
|
+
message,
|
|
35342
|
+
start: span.start,
|
|
35343
|
+
end: span.end,
|
|
35344
|
+
line,
|
|
35345
|
+
column
|
|
35346
|
+
});
|
|
35347
|
+
};
|
|
35348
|
+
const action = ctx.action;
|
|
35349
|
+
const entity = ctx.currentEntity ?? null;
|
|
35350
|
+
const isBatch = (action?.executionMode ?? "single") === "batch";
|
|
35351
|
+
const hasColumn = columnMatcher(entity?.columns);
|
|
35352
|
+
if (text.trim() === "") {
|
|
34489
35353
|
add(
|
|
35354
|
+
{ start: 0, end: text.length },
|
|
35355
|
+
"A DSL body needs at least an execute: block \u2014 an empty file installs an action that does nothing.",
|
|
34490
35356
|
"error",
|
|
34491
|
-
"
|
|
34492
|
-
"no `execute:` block \u2014 it is the only required block in a .dsl file."
|
|
35357
|
+
"dsl/empty-script"
|
|
34493
35358
|
);
|
|
35359
|
+
return out;
|
|
34494
35360
|
}
|
|
34495
|
-
|
|
34496
|
-
|
|
35361
|
+
const firstToken = parsed.tokens[0];
|
|
35362
|
+
const firstLabel = parsed.blocks[0]?.label ?? (firstToken ? { start: firstToken.start, end: firstToken.end } : { start: 0, end: text.length });
|
|
35363
|
+
if (!parsed.blocks.some((b) => b.kind === "execute")) {
|
|
35364
|
+
add(
|
|
35365
|
+
firstLabel,
|
|
35366
|
+
"No execute: block \u2014 it is the only required block, and the compiler runs nothing without it.",
|
|
35367
|
+
"error",
|
|
35368
|
+
"dsl/missing-execute"
|
|
35369
|
+
);
|
|
35370
|
+
}
|
|
35371
|
+
const executeBlock = parsed.blocks.find((b) => b.kind === "execute");
|
|
35372
|
+
if (executeBlock) {
|
|
35373
|
+
for (let i = 0; i < parsed.tokens.length; i++) {
|
|
35374
|
+
const token = parsed.tokens[i];
|
|
35375
|
+
if (token.start <= executeBlock.label.start) continue;
|
|
35376
|
+
if (token.character !== 0) continue;
|
|
35377
|
+
const label = blockLabelAt(parsed.tokens, i);
|
|
35378
|
+
if (!label) continue;
|
|
34497
35379
|
add(
|
|
34498
|
-
|
|
34499
|
-
"
|
|
34500
|
-
|
|
34501
|
-
|
|
35380
|
+
label.span,
|
|
35381
|
+
label.kind === "execute" ? `"execute:" is declared more than once \u2014 the first one's body runs to the end of the file, so this header and everything under it are swallowed into it and parsed as JavaScript. Merge the two into one execute: block.` : `"${label.kind}:" is below execute:, whose body runs to the end of the file \u2014 the header and everything under it are swallowed into the execute: script and parsed as JavaScript. Move it above execute:.`,
|
|
35382
|
+
"error",
|
|
35383
|
+
"dsl/block-after-execute"
|
|
34502
35384
|
);
|
|
34503
35385
|
}
|
|
34504
35386
|
}
|
|
34505
|
-
const
|
|
34506
|
-
|
|
34507
|
-
|
|
34508
|
-
if (
|
|
35387
|
+
const seenBlocks = /* @__PURE__ */ new Set();
|
|
35388
|
+
for (let i = 0; i < parsed.blocks.length; i++) {
|
|
35389
|
+
const block = parsed.blocks[i];
|
|
35390
|
+
if (seenBlocks.has(block.kind)) {
|
|
34509
35391
|
add(
|
|
35392
|
+
block.label,
|
|
35393
|
+
`"${block.kind}:" is declared more than once \u2014 the compiler keeps one and silently drops the other.`,
|
|
34510
35394
|
"error",
|
|
34511
|
-
"block
|
|
34512
|
-
|
|
35395
|
+
"dsl/duplicate-block"
|
|
35396
|
+
);
|
|
35397
|
+
continue;
|
|
35398
|
+
}
|
|
35399
|
+
seenBlocks.add(block.kind);
|
|
35400
|
+
const prev = parsed.blocks[i - 1];
|
|
35401
|
+
if (prev && prev.kind !== "execute" && !BLOCK_STOPS[prev.kind].has(block.kind)) {
|
|
35402
|
+
add(
|
|
35403
|
+
block.label,
|
|
35404
|
+
`"${block.kind}:" must come before "${prev.kind}:" \u2014 the compiler reads ${prev.kind}: up to the next header it recognises, and ${block.kind}: is not one of them, so this header and its body end up inside the ${prev.kind}: body.`,
|
|
35405
|
+
"error",
|
|
35406
|
+
"dsl/block-order"
|
|
35407
|
+
);
|
|
35408
|
+
}
|
|
35409
|
+
}
|
|
35410
|
+
for (const ref of parsed.fieldRefs) {
|
|
35411
|
+
if (ref.block === "schema" || ref.block === "params") continue;
|
|
35412
|
+
const isDslBlock = ref.block === "execute" || ref.block === "onBeforeStart";
|
|
35413
|
+
const secondRef = ref.navigation.findIndex((h, idx) => idx > 0 && h.bracketed);
|
|
35414
|
+
if (isDslBlock && secondRef > 0) {
|
|
35415
|
+
const first = ref.navigation[secondRef];
|
|
35416
|
+
const last = ref.navigation[ref.navigation.length - 1];
|
|
35417
|
+
add(
|
|
35418
|
+
{ start: first.span.start, end: last.span.end },
|
|
35419
|
+
`Ref navigation is single-hop in the DSL. Use getRecord() or select() to reach past "${ref.navigation[0].name}".`,
|
|
35420
|
+
"error",
|
|
35421
|
+
"dsl/multi-hop-nav"
|
|
34513
35422
|
);
|
|
34514
|
-
break;
|
|
34515
35423
|
}
|
|
35424
|
+
if (isBatch && ref.block === "execute" && LITERAL_NAMES.has(ref.name))
|
|
35425
|
+
continue;
|
|
35426
|
+
if (!entity) continue;
|
|
35427
|
+
if (entity.columns.size === 0) continue;
|
|
35428
|
+
if (hasColumn(ref.name)) continue;
|
|
35429
|
+
if (ref.isCurrentRecord && parsed.locals.has(ref.name)) continue;
|
|
35430
|
+
add(
|
|
35431
|
+
ref.span,
|
|
35432
|
+
`"${ref.name}" is not a column on ${entity.qualified}. [field] names are checked at pack time \u2014 this is a compile error, not an empty read.`,
|
|
35433
|
+
"error",
|
|
35434
|
+
"dsl/unknown-column"
|
|
35435
|
+
);
|
|
34516
35436
|
}
|
|
34517
|
-
|
|
34518
|
-
|
|
34519
|
-
|
|
35437
|
+
if (entity && entity.columns.size > 0) {
|
|
35438
|
+
for (const ref of parsed.globalRefs) {
|
|
35439
|
+
if (ref.global !== "old") continue;
|
|
35440
|
+
if (ref.block !== "execute" && ref.block !== "onBeforeStart") continue;
|
|
35441
|
+
if (hasColumn(ref.property)) continue;
|
|
35442
|
+
add(
|
|
35443
|
+
ref.span,
|
|
35444
|
+
`"${ref.property}" is not a column on ${entity.qualified}. old[field] reads the pre-change value of a column, and the name is checked at pack time.`,
|
|
35445
|
+
"error",
|
|
35446
|
+
"dsl/unknown-column"
|
|
35447
|
+
);
|
|
34520
35448
|
}
|
|
34521
35449
|
}
|
|
34522
|
-
const
|
|
34523
|
-
const
|
|
34524
|
-
const
|
|
34525
|
-
|
|
34526
|
-
|
|
34527
|
-
|
|
34528
|
-
|
|
35450
|
+
const declared = new Set(parsed.params.map((p) => p.name.toLowerCase()));
|
|
35451
|
+
const hasParamsBlock = parsed.blocks.some((b) => b.kind === "params");
|
|
35452
|
+
for (const ref of parsed.globalRefs) {
|
|
35453
|
+
if (ref.global !== "params") continue;
|
|
35454
|
+
if (!hasParamsBlock || declared.has(ref.property.toLowerCase())) continue;
|
|
35455
|
+
add(
|
|
35456
|
+
ref.span,
|
|
35457
|
+
`"${ref.property}" is not declared in the params: block.`,
|
|
35458
|
+
"error",
|
|
35459
|
+
"dsl/unknown-param"
|
|
35460
|
+
);
|
|
35461
|
+
}
|
|
35462
|
+
if (action) {
|
|
35463
|
+
const mode = action.executionMode ?? "single";
|
|
35464
|
+
const recordsSpan = parsed.locals.has("records") ? void 0 : parsed.recordsRefs[0];
|
|
35465
|
+
const bareFields = parsed.fieldRefs.filter(
|
|
35466
|
+
(r) => r.block === "execute" && r.isCurrentRecord && !parsed.locals.has(r.name) && !LITERAL_NAMES.has(r.name)
|
|
35467
|
+
);
|
|
35468
|
+
if (mode === "batch") {
|
|
35469
|
+
for (const field of bareFields) {
|
|
34529
35470
|
add(
|
|
35471
|
+
field.outerSpan,
|
|
35472
|
+
`Action "${action.code}" runs in batch mode, which has no current record. Iterate instead: for x in records { x[${field.name}] }.`,
|
|
34530
35473
|
"error",
|
|
34531
|
-
"
|
|
34532
|
-
`the execute: block calls ${fn}(), which is undefined at runtime \u2014 install fails with "'${fn}' is not defined". Use ${replacement} in execute:. ${fn}() is formula-only (canExecute:, formula columns).`,
|
|
34533
|
-
lineOf(src, execOffset + (m.index ?? 0))
|
|
35474
|
+
"dsl/batch-bare-field"
|
|
34534
35475
|
);
|
|
34535
35476
|
}
|
|
34536
35477
|
}
|
|
34537
|
-
|
|
35478
|
+
if (mode !== "batch" && recordsSpan) {
|
|
34538
35479
|
add(
|
|
34539
|
-
|
|
34540
|
-
"
|
|
34541
|
-
"
|
|
34542
|
-
|
|
35480
|
+
recordsSpan,
|
|
35481
|
+
`"records" only exists in batch mode; action "${action.code}" runs in "${mode}" mode.`,
|
|
35482
|
+
"warning",
|
|
35483
|
+
"dsl/records-outside-batch"
|
|
34543
35484
|
);
|
|
34544
35485
|
}
|
|
34545
35486
|
}
|
|
34546
|
-
const
|
|
34547
|
-
|
|
34548
|
-
|
|
34549
|
-
const first = recordRefs[0];
|
|
35487
|
+
for (const token of parsed.tokens) {
|
|
35488
|
+
if (blockOf(parsed, token.start) !== "canExecute") continue;
|
|
35489
|
+
if (token.text !== "&&" && token.text !== "||") continue;
|
|
34550
35490
|
add(
|
|
34551
|
-
|
|
34552
|
-
"
|
|
34553
|
-
|
|
34554
|
-
|
|
35491
|
+
{ start: token.start, end: token.end },
|
|
35492
|
+
`canExecute is a formula, not JavaScript \u2014 write ${token.text === "&&" ? "AND" : "OR"}.`,
|
|
35493
|
+
"warning",
|
|
35494
|
+
"dsl/js-in-formula"
|
|
34555
35495
|
);
|
|
34556
35496
|
}
|
|
34557
|
-
|
|
34558
|
-
|
|
34559
|
-
(m) => (m.index ?? 0) >= execBlock.start && (m.index ?? 0) < execBlock.end
|
|
34560
|
-
);
|
|
34561
|
-
if (inExec.length > 0) {
|
|
35497
|
+
for (const call of parsed.calls) {
|
|
35498
|
+
if (call.name === "userId") {
|
|
34562
35499
|
add(
|
|
35500
|
+
call.nameSpan,
|
|
35501
|
+
"'userId' is a value, not a function \u2014 write userId without parentheses, or currentUserId(). The compiler's bare-identifier rewrite fires either way, so userId() reaches Jint as a call against a number.",
|
|
34563
35502
|
"error",
|
|
34564
|
-
"
|
|
34565
|
-
`executionMode is 'batch' but execute: uses record-context [${inExec[0][1]}] \u2014 in batch mode there is no current record. Iterate the selection instead: \`for rec in records { rec[field] }\`.`,
|
|
34566
|
-
lineOf(src, inExec[0].index ?? 0)
|
|
35503
|
+
"dsl/user-id-call"
|
|
34567
35504
|
);
|
|
35505
|
+
continue;
|
|
35506
|
+
}
|
|
35507
|
+
const builtin = BUILTIN_BY_NAME.get(call.name);
|
|
35508
|
+
if (!builtin || builtin.entityArg === void 0) continue;
|
|
35509
|
+
const arg = call.firstStringArg;
|
|
35510
|
+
if (!arg) continue;
|
|
35511
|
+
if (arg.interpolated) continue;
|
|
35512
|
+
if (arg.value.includes(".") || ctx.moduleCode?.includes("-")) continue;
|
|
35513
|
+
add(
|
|
35514
|
+
arg.span,
|
|
35515
|
+
`Qualify the entity code as "${ctx.moduleCode ? `${ctx.moduleCode}.` : "<module>."}${arg.value}" \u2014 entity_cd is only unique per module, so a bare code can resolve to another module's entity.`,
|
|
35516
|
+
"info",
|
|
35517
|
+
"dsl/unqualified-entity"
|
|
35518
|
+
);
|
|
35519
|
+
}
|
|
35520
|
+
let depth = 0;
|
|
35521
|
+
const functionDepths = [];
|
|
35522
|
+
const parenStack = [];
|
|
35523
|
+
let matchingParenOpen = -1;
|
|
35524
|
+
for (let i = 0; i < parsed.tokens.length; i++) {
|
|
35525
|
+
const token = parsed.tokens[i];
|
|
35526
|
+
if (token.text === "(") parenStack.push(i);
|
|
35527
|
+
else if (token.text === ")") matchingParenOpen = parenStack.pop() ?? -1;
|
|
35528
|
+
if (token.text === "{") {
|
|
35529
|
+
depth++;
|
|
35530
|
+
if (opensFunctionBody(parsed.tokens, i, matchingParenOpen))
|
|
35531
|
+
functionDepths.push(depth);
|
|
35532
|
+
} else if (token.text === "}") {
|
|
35533
|
+
if (functionDepths[functionDepths.length - 1] === depth)
|
|
35534
|
+
functionDepths.pop();
|
|
35535
|
+
depth = Math.max(0, depth - 1);
|
|
35536
|
+
}
|
|
35537
|
+
if (depth === 0 && token.character > 0) {
|
|
35538
|
+
const label = blockLabelAt(parsed.tokens, i);
|
|
35539
|
+
const isParamDecl = parsed.params.some(
|
|
35540
|
+
(p) => p.nameSpan.start === token.start
|
|
35541
|
+
);
|
|
35542
|
+
if (label && !isParamDecl) {
|
|
35543
|
+
add(
|
|
35544
|
+
label.span,
|
|
35545
|
+
`Block headers start at column 0. Indented, "${label.kind}:" is body text \u2014 the compiler never opens the block, and it runs as if empty.`,
|
|
35546
|
+
"error",
|
|
35547
|
+
"dsl/indented-block-header"
|
|
35548
|
+
);
|
|
35549
|
+
}
|
|
34568
35550
|
}
|
|
35551
|
+
if (token.kind !== "ident" || token.text !== "return") continue;
|
|
35552
|
+
if (parsed.tokens[i - 1]?.text === ".") continue;
|
|
35553
|
+
if (parsed.tokens[i + 1]?.text === ":") continue;
|
|
35554
|
+
if (isMethodName(parsed.tokens, i)) continue;
|
|
35555
|
+
if (functionDepths.length > 0) continue;
|
|
35556
|
+
const block = blockOf(parsed, token.start);
|
|
35557
|
+
if (block !== "execute" && block !== "onBeforeStart") continue;
|
|
35558
|
+
add(
|
|
35559
|
+
{ start: token.start, end: token.end },
|
|
35560
|
+
`A return outside a function is a syntax error \u2014 ${block}: compiles to a bare script. Use exit(message, level) to stop early and keep the changes made so far.`,
|
|
35561
|
+
"error",
|
|
35562
|
+
"dsl/top-level-return"
|
|
35563
|
+
);
|
|
35564
|
+
}
|
|
35565
|
+
for (const call of parsed.calls) {
|
|
35566
|
+
const replacement = FORMULA_ONLY.get(call.name);
|
|
35567
|
+
if (!replacement) continue;
|
|
35568
|
+
if (call.block !== "execute" && call.block !== "onBeforeStart") continue;
|
|
35569
|
+
add(
|
|
35570
|
+
call.nameSpan,
|
|
35571
|
+
`${call.name}() is formula-only \u2014 in ${call.block}: it is undefined and install fails with "'${call.name}' is not defined". Write ${replacement}.`,
|
|
35572
|
+
"error",
|
|
35573
|
+
"dsl/formula-only-function"
|
|
35574
|
+
);
|
|
34569
35575
|
}
|
|
34570
|
-
|
|
34571
|
-
|
|
35576
|
+
const tokenAt = new Map(parsed.tokens.map((t, i) => [t.start, i]));
|
|
35577
|
+
for (const call of parsed.calls) {
|
|
35578
|
+
if (call.name !== "query" && call.name !== "callProc") continue;
|
|
35579
|
+
const arg = call.firstStringArg;
|
|
35580
|
+
if (!arg) continue;
|
|
35581
|
+
const bad = /(^|[^:\w]):([a-z][a-z0-9_]*)/i.exec(stripSqlNoise(arg.value));
|
|
34572
35582
|
if (bad) {
|
|
35583
|
+
const name = bad[2];
|
|
34573
35584
|
add(
|
|
35585
|
+
arg.span,
|
|
35586
|
+
`SQL binds @name, not :name \u2014 rewrite ':${name}' as '@${name}' and pass { ${name}: value } as the params argument.`,
|
|
34574
35587
|
"error",
|
|
34575
|
-
"sql-placeholder"
|
|
34576
|
-
`SQL uses ':${bad[1]}' as a placeholder \u2014 dForge binds '@${bad[1]}'. Rewrite as '@${bad[1]}' and pass { ${bad[1]}: value } as the params argument.`,
|
|
34577
|
-
lineOf(src, index)
|
|
35588
|
+
"dsl/sql-placeholder"
|
|
34578
35589
|
);
|
|
34579
35590
|
}
|
|
34580
|
-
|
|
34581
|
-
|
|
35591
|
+
if (arg.interpolated || parsed.tokens[arg.endIndex + 1]?.text === "+") {
|
|
35592
|
+
const built = arg.interpolated ? "${\u2026} interpolation" : "concatenation";
|
|
34582
35593
|
add(
|
|
35594
|
+
arg.span,
|
|
35595
|
+
// callProc's first argument is the procedure name, and an
|
|
35596
|
+
// identifier cannot be bound — the @placeholder advice cannot
|
|
35597
|
+
// be followed there.
|
|
35598
|
+
call.name === "callProc" ? `Procedure name built by ${built} \u2014 a name is an identifier, so it cannot be bound as a @placeholder. Choose between fixed names in the script rather than splicing a value into one.` : `SQL built by ${built} \u2014 pass @placeholders and a params object instead, so values are bound rather than spliced into the statement.`,
|
|
34583
35599
|
"warning",
|
|
34584
|
-
"sql-concat"
|
|
34585
|
-
"SQL string looks concatenated \u2014 build queries with @placeholders + a params object, never by string concatenation.",
|
|
34586
|
-
lineOf(src, index)
|
|
35600
|
+
"dsl/sql-concat"
|
|
34587
35601
|
);
|
|
34588
35602
|
}
|
|
34589
35603
|
}
|
|
34590
|
-
|
|
34591
|
-
|
|
34592
|
-
const
|
|
34593
|
-
|
|
35604
|
+
const declaredFunctions = /* @__PURE__ */ new Map();
|
|
35605
|
+
for (let i = 0; i < parsed.tokens.length; i++) {
|
|
35606
|
+
const token = parsed.tokens[i];
|
|
35607
|
+
if (token.text !== "function") continue;
|
|
35608
|
+
const name = parsed.tokens[i + 1];
|
|
35609
|
+
if (name?.kind !== "ident") continue;
|
|
35610
|
+
const blocks = declaredFunctions.get(name.text) ?? /* @__PURE__ */ new Set();
|
|
35611
|
+
blocks.add(blockOf(parsed, token.start));
|
|
35612
|
+
declaredFunctions.set(name.text, blocks);
|
|
35613
|
+
}
|
|
35614
|
+
const isDeclaredIn = (name, block) => {
|
|
35615
|
+
const blocks = declaredFunctions.get(name);
|
|
35616
|
+
return blocks !== void 0 && (blocks.has(null) || blocks.has(block));
|
|
35617
|
+
};
|
|
35618
|
+
const reportedUnknown = /* @__PURE__ */ new Set();
|
|
35619
|
+
for (const call of parsed.calls) {
|
|
35620
|
+
if (call.block !== "execute" && call.block !== "onBeforeStart") continue;
|
|
35621
|
+
const name = call.name;
|
|
35622
|
+
const idx = tokenAt.get(call.nameSpan.start);
|
|
35623
|
+
if (idx !== void 0) {
|
|
35624
|
+
const prev = parsed.tokens[idx - 1]?.text;
|
|
35625
|
+
if (prev === "function" || prev === "new") continue;
|
|
35626
|
+
if (isMethodName(parsed.tokens, idx)) continue;
|
|
35627
|
+
}
|
|
35628
|
+
if (BUILTIN_BY_NAME.has(name) || CONTROL_KEYWORDS.has(name) || KEYWORDS_BEFORE_PAREN.has(name) || JS_GLOBALS.has(name) || FORMULA_ONLY.has(name) || // Its own rule reports this one, with better wording.
|
|
35629
|
+
name === "userId" || isDeclaredIn(name, call.block) || parsed.locals.has(name) || reportedUnknown.has(name)) {
|
|
35630
|
+
continue;
|
|
35631
|
+
}
|
|
35632
|
+
reportedUnknown.add(name);
|
|
35633
|
+
add(
|
|
35634
|
+
call.nameSpan,
|
|
35635
|
+
`"${name}()" is not a DSL host function, and nothing in this script declares it. Check it against the built-in catalog.`,
|
|
35636
|
+
"warning",
|
|
35637
|
+
"dsl/unknown-builtin"
|
|
35638
|
+
);
|
|
35639
|
+
}
|
|
35640
|
+
if (ctx.action?.viaJob) {
|
|
35641
|
+
const inScript = (block) => block === "execute" || block === "onBeforeStart";
|
|
35642
|
+
const bound = [];
|
|
35643
|
+
for (const ref of parsed.fieldRefs) {
|
|
35644
|
+
if (!inScript(ref.block)) continue;
|
|
35645
|
+
if (isBatch && ref.block === "execute" && LITERAL_NAMES.has(ref.name))
|
|
35646
|
+
continue;
|
|
35647
|
+
if (ref.isCurrentRecord && parsed.locals.has(ref.name)) continue;
|
|
35648
|
+
bound.push({ span: ref.outerSpan, read: `[${ref.name}]` });
|
|
35649
|
+
}
|
|
35650
|
+
for (const ref of parsed.globalRefs) {
|
|
35651
|
+
if (ref.global !== "old" || !inScript(ref.block)) continue;
|
|
35652
|
+
bound.push({ span: ref.span, read: `old[${ref.property}]` });
|
|
35653
|
+
}
|
|
35654
|
+
if (!parsed.locals.has("records")) {
|
|
35655
|
+
for (const span of parsed.recordsRefs) {
|
|
35656
|
+
if (!inScript(blockOf(parsed, span.start))) continue;
|
|
35657
|
+
bound.push({ span, read: "records" });
|
|
35658
|
+
}
|
|
35659
|
+
}
|
|
35660
|
+
const first = bound.sort((a, b) => a.span.start - b.span.start)[0];
|
|
35661
|
+
if (first) {
|
|
34594
35662
|
add(
|
|
35663
|
+
first.span,
|
|
35664
|
+
`Action "${ctx.action.code}" is invoked by a scheduled job, which runs as the system user with no record bound \u2014 ${first.read} has nothing to read. Fetch rows with select() or query() instead.`,
|
|
34595
35665
|
"error",
|
|
34596
|
-
"
|
|
34597
|
-
"top-level `return` is invalid in a DSL body. Use exit('msg', 'info') to stop and commit, or error('msg') to stop and roll back.",
|
|
34598
|
-
lineOf(src, execOffset + (m.index ?? 0))
|
|
35666
|
+
"dsl/job-record-context"
|
|
34599
35667
|
);
|
|
34600
35668
|
}
|
|
34601
35669
|
}
|
|
34602
|
-
|
|
34603
|
-
|
|
34604
|
-
|
|
34605
|
-
|
|
34606
|
-
|
|
34607
|
-
|
|
34608
|
-
|
|
35670
|
+
out.push(...inlineAssignmentIssues(parsed, positionAt));
|
|
35671
|
+
return out;
|
|
35672
|
+
}
|
|
35673
|
+
function columnMatcher(columns) {
|
|
35674
|
+
if (!columns) return () => true;
|
|
35675
|
+
if (typeof columns.keys !== "function")
|
|
35676
|
+
return (name) => columns.has(name) || columns.has(name.toLowerCase());
|
|
35677
|
+
const folded = /* @__PURE__ */ new Set();
|
|
35678
|
+
for (const key of columns.keys()) folded.add(key.toLowerCase());
|
|
35679
|
+
return (name) => folded.has(name.toLowerCase());
|
|
35680
|
+
}
|
|
35681
|
+
function isMethodName(tokens, i) {
|
|
35682
|
+
if (tokens[i + 1]?.text !== "(") return false;
|
|
35683
|
+
let depth = 0;
|
|
35684
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
35685
|
+
const text = tokens[j].text;
|
|
35686
|
+
if (text === "(") depth++;
|
|
35687
|
+
else if (text === ")") {
|
|
35688
|
+
depth--;
|
|
35689
|
+
if (depth === 0) return tokens[j + 1]?.text === "{";
|
|
35690
|
+
}
|
|
35691
|
+
}
|
|
35692
|
+
return false;
|
|
35693
|
+
}
|
|
35694
|
+
function opensFunctionBody(tokens, i, matchingParenOpen) {
|
|
35695
|
+
const prev = tokens[i - 1];
|
|
35696
|
+
if (prev?.text === "=>") return true;
|
|
35697
|
+
if (prev?.text !== ")" || matchingParenOpen < 0) return false;
|
|
35698
|
+
const head = tokens[matchingParenOpen - 1];
|
|
35699
|
+
return head?.kind === "ident" && !CONTROL_KEYWORDS.has(head.text);
|
|
35700
|
+
}
|
|
35701
|
+
var BLOCK_STOPS = {
|
|
35702
|
+
params: /* @__PURE__ */ new Set(["schema", "canExecute", "onBeforeStart", "execute"]),
|
|
35703
|
+
canExecute: /* @__PURE__ */ new Set(["schema", "onBeforeStart", "execute"]),
|
|
35704
|
+
schema: /* @__PURE__ */ new Set(["params", "canExecute", "onBeforeStart", "execute"]),
|
|
35705
|
+
onBeforeStart: /* @__PURE__ */ new Set(["schema", "execute"]),
|
|
35706
|
+
execute: /* @__PURE__ */ new Set()
|
|
35707
|
+
};
|
|
35708
|
+
var FORMULA_ONLY = /* @__PURE__ */ new Map([
|
|
35709
|
+
["TODAY", "now()"],
|
|
35710
|
+
["NOW", "now()"],
|
|
35711
|
+
["CURRENT_USER_ID", "currentUserId()"]
|
|
35712
|
+
]);
|
|
35713
|
+
var KEYWORDS_BEFORE_PAREN = /* @__PURE__ */ new Set([
|
|
35714
|
+
"function",
|
|
35715
|
+
"return",
|
|
35716
|
+
"typeof",
|
|
35717
|
+
"instanceof",
|
|
35718
|
+
"new",
|
|
35719
|
+
"delete",
|
|
35720
|
+
"void",
|
|
35721
|
+
"in",
|
|
35722
|
+
"of",
|
|
35723
|
+
"do",
|
|
35724
|
+
"else",
|
|
35725
|
+
"try",
|
|
35726
|
+
"throw",
|
|
35727
|
+
"yield",
|
|
35728
|
+
"await"
|
|
35729
|
+
]);
|
|
35730
|
+
var JS_GLOBALS = /* @__PURE__ */ new Set([
|
|
35731
|
+
"parseInt",
|
|
35732
|
+
"parseFloat",
|
|
35733
|
+
"isNaN",
|
|
35734
|
+
"isFinite",
|
|
35735
|
+
"encodeURI",
|
|
35736
|
+
"decodeURI",
|
|
35737
|
+
"encodeURIComponent",
|
|
35738
|
+
"decodeURIComponent",
|
|
35739
|
+
"String",
|
|
35740
|
+
"Number",
|
|
35741
|
+
"Boolean",
|
|
35742
|
+
"Array",
|
|
35743
|
+
"Object",
|
|
35744
|
+
"Date",
|
|
35745
|
+
"Math",
|
|
35746
|
+
"JSON",
|
|
35747
|
+
"RegExp",
|
|
35748
|
+
"Error",
|
|
35749
|
+
"Set",
|
|
35750
|
+
"Map",
|
|
35751
|
+
"WeakSet",
|
|
35752
|
+
"WeakMap",
|
|
35753
|
+
"Promise",
|
|
35754
|
+
"Symbol",
|
|
35755
|
+
"BigInt"
|
|
35756
|
+
]);
|
|
35757
|
+
var LITERAL_NAMES = /* @__PURE__ */ new Set([
|
|
35758
|
+
"true",
|
|
35759
|
+
"false",
|
|
35760
|
+
"null",
|
|
35761
|
+
"TRUE",
|
|
35762
|
+
"FALSE",
|
|
35763
|
+
"NULL"
|
|
35764
|
+
]);
|
|
35765
|
+
function stripSqlNoise(sql) {
|
|
35766
|
+
let out = "";
|
|
35767
|
+
let i = 0;
|
|
35768
|
+
while (i < sql.length) {
|
|
35769
|
+
const c = sql[i];
|
|
35770
|
+
if (c === "'" || c === '"') {
|
|
35771
|
+
const quote = c;
|
|
35772
|
+
out += " ";
|
|
35773
|
+
i++;
|
|
35774
|
+
while (i < sql.length) {
|
|
35775
|
+
if (sql[i] === quote && sql[i + 1] === quote) {
|
|
35776
|
+
out += " ";
|
|
35777
|
+
i += 2;
|
|
35778
|
+
continue;
|
|
35779
|
+
}
|
|
35780
|
+
if (sql[i] === quote) break;
|
|
35781
|
+
out += sql[i] === "\n" ? "\n" : " ";
|
|
35782
|
+
i++;
|
|
35783
|
+
}
|
|
35784
|
+
if (i < sql.length) {
|
|
35785
|
+
out += " ";
|
|
35786
|
+
i++;
|
|
35787
|
+
}
|
|
34609
35788
|
continue;
|
|
34610
35789
|
}
|
|
34611
|
-
|
|
34612
|
-
|
|
34613
|
-
|
|
34614
|
-
|
|
34615
|
-
|
|
34616
|
-
|
|
34617
|
-
|
|
35790
|
+
if (c === "-" && sql[i + 1] === "-") {
|
|
35791
|
+
while (i < sql.length && sql[i] !== "\n") {
|
|
35792
|
+
out += " ";
|
|
35793
|
+
i++;
|
|
35794
|
+
}
|
|
35795
|
+
continue;
|
|
35796
|
+
}
|
|
35797
|
+
if (c === "/" && sql[i + 1] === "*") {
|
|
35798
|
+
out += " ";
|
|
35799
|
+
i += 2;
|
|
35800
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
|
|
35801
|
+
out += sql[i] === "\n" ? "\n" : " ";
|
|
35802
|
+
i++;
|
|
35803
|
+
}
|
|
35804
|
+
if (i < sql.length) {
|
|
35805
|
+
out += " ";
|
|
35806
|
+
i += 2;
|
|
35807
|
+
}
|
|
35808
|
+
continue;
|
|
35809
|
+
}
|
|
35810
|
+
out += c;
|
|
35811
|
+
i++;
|
|
35812
|
+
}
|
|
35813
|
+
return out;
|
|
35814
|
+
}
|
|
35815
|
+
function blockOf(parsed, offset) {
|
|
35816
|
+
return parsed.blocks.find((b) => offset >= b.body.start && offset < b.body.end)?.kind ?? null;
|
|
35817
|
+
}
|
|
35818
|
+
function inlineAssignmentIssues(parsed, positionAt) {
|
|
35819
|
+
const out = [];
|
|
35820
|
+
for (const ref of parsed.fieldRefs) {
|
|
35821
|
+
const assign = parsed.tokens[ref.endIndex + 1];
|
|
35822
|
+
if (!assign || assign.kind !== "punct" || assign.text !== "=") continue;
|
|
35823
|
+
const line = parsed.tokens[ref.startIndex].line;
|
|
35824
|
+
let openedHere = false;
|
|
35825
|
+
for (let i = ref.startIndex - 1; i >= 0; i--) {
|
|
35826
|
+
const t = parsed.tokens[i];
|
|
35827
|
+
if (t.line !== line) break;
|
|
35828
|
+
if (t.text === "{") {
|
|
35829
|
+
openedHere = true;
|
|
35830
|
+
break;
|
|
35831
|
+
}
|
|
35832
|
+
}
|
|
35833
|
+
if (!openedHere) continue;
|
|
35834
|
+
const pos = positionAt(ref.outerSpan.start);
|
|
35835
|
+
out.push({
|
|
35836
|
+
rule: "dsl/inline-assignment",
|
|
35837
|
+
severity: "warning",
|
|
35838
|
+
message: "An assignment to a record field on the same line as the brace that opens its block mis-compiles. Put the assignment on its own line.",
|
|
35839
|
+
start: ref.outerSpan.start,
|
|
35840
|
+
end: ref.outerSpan.end,
|
|
35841
|
+
line: pos.line,
|
|
35842
|
+
column: pos.column
|
|
35843
|
+
});
|
|
35844
|
+
}
|
|
35845
|
+
return out;
|
|
35846
|
+
}
|
|
35847
|
+
function lineIndexer(text) {
|
|
35848
|
+
const starts = [0];
|
|
35849
|
+
for (let i = 0; i < text.length; i++) {
|
|
35850
|
+
if (text[i] === "\n") starts.push(i + 1);
|
|
35851
|
+
}
|
|
35852
|
+
return (offset) => {
|
|
35853
|
+
let lo = 0;
|
|
35854
|
+
let hi = starts.length - 1;
|
|
35855
|
+
while (lo < hi) {
|
|
35856
|
+
const mid = lo + hi + 1 >> 1;
|
|
35857
|
+
if (starts[mid] <= offset) lo = mid;
|
|
35858
|
+
else hi = mid - 1;
|
|
35859
|
+
}
|
|
35860
|
+
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
35861
|
+
};
|
|
35862
|
+
}
|
|
35863
|
+
|
|
35864
|
+
// src/tools/dsl-check.ts
|
|
35865
|
+
function checkDsl2(src, opts = {}) {
|
|
35866
|
+
const raw = opts.executionMode?.trim();
|
|
35867
|
+
const mode = raw?.toLowerCase();
|
|
35868
|
+
const executionMode = mode === "single" || mode === "each" || mode === "batch" ? mode : void 0;
|
|
35869
|
+
const ctx = {
|
|
35870
|
+
moduleCode: opts.moduleCode,
|
|
35871
|
+
action: executionMode || opts.viaJob || opts.actionCode ? { code: opts.actionCode ?? "this action", executionMode, viaJob: opts.viaJob } : void 0,
|
|
35872
|
+
currentEntity: opts.entity ?? null
|
|
35873
|
+
};
|
|
35874
|
+
const issues = checkDsl(src, ctx).map(toIssue);
|
|
35875
|
+
if (raw && !executionMode) {
|
|
35876
|
+
issues.unshift({
|
|
35877
|
+
level: "warning",
|
|
35878
|
+
rule: "action/unknown-execution-mode",
|
|
35879
|
+
message: `executionMode '${raw}' is not one of single, each, batch \u2014 the record-context rules stood down, so a \`[field]\` read that batch mode forbids would not be reported. Fix the value in ui/actions.json.`
|
|
35880
|
+
});
|
|
34618
35881
|
}
|
|
34619
35882
|
return issues;
|
|
34620
35883
|
}
|
|
35884
|
+
function toIssue(issue2) {
|
|
35885
|
+
return {
|
|
35886
|
+
level: issue2.severity === "error" ? "error" : "warning",
|
|
35887
|
+
rule: issue2.rule,
|
|
35888
|
+
message: issue2.message,
|
|
35889
|
+
line: issue2.line
|
|
35890
|
+
};
|
|
35891
|
+
}
|
|
34621
35892
|
|
|
34622
35893
|
// src/tools/action-add.ts
|
|
34623
35894
|
var actionAddSchema = {
|
|
@@ -34640,7 +35911,19 @@ var actionAddSchema = {
|
|
|
34640
35911
|
)
|
|
34641
35912
|
};
|
|
34642
35913
|
function actionAdd(args) {
|
|
34643
|
-
const
|
|
35914
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
35915
|
+
const { traits: localTraits, error: traitsError } = readLocalTraits(paths.root);
|
|
35916
|
+
if (traitsError) {
|
|
35917
|
+
throw new Error(
|
|
35918
|
+
`traits.json \u2014 ${traitsError} Its traits can't be overlaid, so '${args.entityCode}' reads as declaring an unknown trait and the column rules stand down \u2014 this body would be written unchecked against the entity. Fix the file and call again.`
|
|
35919
|
+
);
|
|
35920
|
+
}
|
|
35921
|
+
const dslIssues = checkDsl2(args.dslBody, {
|
|
35922
|
+
executionMode: args.executionMode,
|
|
35923
|
+
moduleCode: manifest.code,
|
|
35924
|
+
actionCode: args.code,
|
|
35925
|
+
entity: entityRecordContext(paths, manifest, args.entityCode, localTraits)
|
|
35926
|
+
});
|
|
34644
35927
|
const dslErrors = dslIssues.filter((i) => i.level === "error");
|
|
34645
35928
|
if (dslErrors.length > 0) {
|
|
34646
35929
|
throw new Error(
|
|
@@ -34649,7 +35932,6 @@ function actionAdd(args) {
|
|
|
34649
35932
|
);
|
|
34650
35933
|
}
|
|
34651
35934
|
const dslWarnings = dslIssues.filter((i) => i.level === "warning");
|
|
34652
|
-
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
34653
35935
|
const actionsJson = readJsonOrDefault(paths.actions, {});
|
|
34654
35936
|
if (Object.prototype.hasOwnProperty.call(actionsJson, args.code)) {
|
|
34655
35937
|
throw new Error(
|
|
@@ -34698,6 +35980,9 @@ var actionCheckSchema = {
|
|
|
34698
35980
|
),
|
|
34699
35981
|
viaJob: external_exports.boolean().optional().describe(
|
|
34700
35982
|
"True when a scheduled job will fire this action \u2014 jobs have NO current record, so '[field]' becomes a hard error. Detected automatically from logic/jobs.json when actionCode is used."
|
|
35983
|
+
),
|
|
35984
|
+
entityCode: external_exports.string().regex(/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/).optional().describe(
|
|
35985
|
+
"Entity the draft body acts on. With moduleDir, this turns on the column rules ('[field]' must be a real column, traits expanded) for a dslBody that isn't registered yet. Read from ui/actions.json when actionCode is used."
|
|
34701
35986
|
)
|
|
34702
35987
|
};
|
|
34703
35988
|
function actionCheck(args) {
|
|
@@ -34705,37 +35990,63 @@ function actionCheck(args) {
|
|
|
34705
35990
|
let mode = args.executionMode;
|
|
34706
35991
|
let viaJob = args.viaJob ?? false;
|
|
34707
35992
|
let label = "draft DSL body";
|
|
34708
|
-
|
|
34709
|
-
|
|
34710
|
-
|
|
34711
|
-
|
|
34712
|
-
|
|
34713
|
-
|
|
34714
|
-
|
|
34715
|
-
|
|
34716
|
-
|
|
34717
|
-
|
|
34718
|
-
|
|
34719
|
-
|
|
34720
|
-
|
|
34721
|
-
|
|
34722
|
-
|
|
34723
|
-
|
|
34724
|
-
|
|
34725
|
-
|
|
34726
|
-
|
|
34727
|
-
|
|
34728
|
-
|
|
34729
|
-
|
|
34730
|
-
|
|
34731
|
-
|
|
34732
|
-
|
|
35993
|
+
let entityCode = args.entityCode;
|
|
35994
|
+
let moduleCode;
|
|
35995
|
+
let entity;
|
|
35996
|
+
const preIssues = [];
|
|
35997
|
+
if (args.actionCode && !args.moduleDir) {
|
|
35998
|
+
throw new Error("moduleDir is required when checking an on-disk action (actionCode).");
|
|
35999
|
+
}
|
|
36000
|
+
if (args.moduleDir) {
|
|
36001
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
36002
|
+
moduleCode = manifest.code;
|
|
36003
|
+
if (args.actionCode) {
|
|
36004
|
+
const actions = readJsonOrDefault(paths.actions, {});
|
|
36005
|
+
const entry = actions[args.actionCode];
|
|
36006
|
+
if (!entry) {
|
|
36007
|
+
throw new Error(
|
|
36008
|
+
`Action '${args.actionCode}' is not in ui/actions.json. Registered actions: ${Object.keys(actions).join(", ") || "(none)"}.`
|
|
36009
|
+
);
|
|
36010
|
+
}
|
|
36011
|
+
const script = entry.script ?? args.actionCode;
|
|
36012
|
+
const dslPath = path10.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
36013
|
+
if (!fs7.existsSync(dslPath)) {
|
|
36014
|
+
throw new Error(
|
|
36015
|
+
`Action '${args.actionCode}' declares script '${script}' but logic/actions/${script}.dsl does not exist.`
|
|
36016
|
+
);
|
|
36017
|
+
}
|
|
36018
|
+
body = fs7.readFileSync(dslPath, "utf8");
|
|
36019
|
+
mode = mode ?? (entry.executionMode ?? entry.mode);
|
|
36020
|
+
entityCode = entityCode ?? entry.entityCode;
|
|
36021
|
+
label = `logic/actions/${script}.dsl`;
|
|
36022
|
+
if (args.viaJob === void 0) {
|
|
36023
|
+
const jobFile = readJsonOrDefault(paths.jobs, {});
|
|
36024
|
+
viaJob = (jobFile.jobs ?? []).some((j) => j.action === args.actionCode);
|
|
36025
|
+
}
|
|
36026
|
+
}
|
|
36027
|
+
const { traits: localTraits, error: traitsError } = readLocalTraits(paths.root);
|
|
36028
|
+
if (traitsError) {
|
|
36029
|
+
preIssues.push({
|
|
36030
|
+
level: "error",
|
|
36031
|
+
rule: "module/invalid-traits-json",
|
|
36032
|
+
message: `traits.json \u2014 ${traitsError} This module's own traits can't be overlaid on the platform ones, so an entity using them reads as declaring an unknown trait and the column rules below stand down. Fix the file, then check again.`
|
|
36033
|
+
});
|
|
34733
36034
|
}
|
|
36035
|
+
entity = entityRecordContext(paths, manifest, entityCode, localTraits);
|
|
34734
36036
|
}
|
|
34735
36037
|
if (body === void 0) {
|
|
34736
36038
|
throw new Error("Pass either dslBody (a draft) or actionCode (an action already on disk).");
|
|
34737
36039
|
}
|
|
34738
|
-
const issues =
|
|
36040
|
+
const issues = [
|
|
36041
|
+
...preIssues,
|
|
36042
|
+
...checkDsl2(body, {
|
|
36043
|
+
executionMode: mode,
|
|
36044
|
+
viaJob,
|
|
36045
|
+
moduleCode,
|
|
36046
|
+
actionCode: args.actionCode,
|
|
36047
|
+
entity
|
|
36048
|
+
})
|
|
36049
|
+
];
|
|
34739
36050
|
const errors = issues.filter((i) => i.level === "error");
|
|
34740
36051
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
34741
36052
|
const summary = errors.length === 0 && warnings.length === 0 ? `\u2713 ${label}: no DSL issues${mode ? ` (executionMode '${mode}'${viaJob ? ", job-invoked" : ""})` : ""}.` : `${label}: ${errors.length} error(s), ${warnings.length} warning(s).${errors.length ? ` First: ${errors[0].message}` : ""}`;
|
|
@@ -34946,6 +36257,12 @@ function buildSkeleton(moduleDir) {
|
|
|
34946
36257
|
const { paths, manifest } = loadManifest(moduleDir);
|
|
34947
36258
|
const entries = [];
|
|
34948
36259
|
const counts = {};
|
|
36260
|
+
const { traits: localTraits, error: traitsError } = readLocalTraits(paths.root);
|
|
36261
|
+
if (traitsError) {
|
|
36262
|
+
throw new Error(
|
|
36263
|
+
`traits.json \u2014 ${traitsError} Its traits can't be overlaid, so the entities that use one would be skipped as declaring an unknown trait and their columns would go unlabelled \u2014 which fails the install. Fix the file and sync again.`
|
|
36264
|
+
);
|
|
36265
|
+
}
|
|
34949
36266
|
const push = (section, pathParts, english, required2 = false) => {
|
|
34950
36267
|
entries.push({ pathParts, english, required: required2 });
|
|
34951
36268
|
counts[section] = (counts[section] ?? 0) + 1;
|
|
@@ -34967,8 +36284,8 @@ function buildSkeleton(moduleDir) {
|
|
|
34967
36284
|
push("entities", ["entities", name, "label"], str(e, "description") ?? titleize(name));
|
|
34968
36285
|
const authored = e.fields ?? {};
|
|
34969
36286
|
const traitCodes = e.traits ?? [];
|
|
34970
|
-
assertKnownTraits(traitCodes, name);
|
|
34971
|
-
const traitFields = expandTraits(traitCodes, name);
|
|
36287
|
+
assertKnownTraits(traitCodes, name, localTraits);
|
|
36288
|
+
const traitFields = expandTraits(traitCodes, name, localTraits);
|
|
34972
36289
|
const all = { ...traitFields, ...authored };
|
|
34973
36290
|
for (const [fname, f] of Object.entries(all)) {
|
|
34974
36291
|
push(
|
|
@@ -35157,21 +36474,27 @@ function seedAdd(args) {
|
|
|
35157
36474
|
);
|
|
35158
36475
|
}
|
|
35159
36476
|
}
|
|
35160
|
-
|
|
35161
|
-
|
|
35162
|
-
|
|
36477
|
+
const { traits: localTraits, error: traitsError } = readLocalTraits(paths.root);
|
|
36478
|
+
if (traitsError) {
|
|
36479
|
+
throw new Error(
|
|
36480
|
+
`traits.json \u2014 ${traitsError} Its traits can't be overlaid, so an entity that uses one reads as declaring an unknown trait and its columns as unknown. Fix the file.`
|
|
36481
|
+
);
|
|
36482
|
+
}
|
|
36483
|
+
assertKnownTraits(traits2, args.entity, localTraits);
|
|
36484
|
+
const { columns: known, fieldDefs: allFields } = expandedEntity(entity, args.entity, localTraits);
|
|
36485
|
+
const traitOnly = [...known].filter((c) => !(c in fields));
|
|
35163
36486
|
known.add(pk);
|
|
35164
36487
|
const unknown2 = /* @__PURE__ */ new Set();
|
|
35165
36488
|
for (const r of args.records) {
|
|
35166
36489
|
for (const k of Object.keys(r)) if (!known.has(k)) unknown2.add(k);
|
|
35167
36490
|
}
|
|
35168
36491
|
if (unknown2.size > 0) {
|
|
35169
|
-
const traitCols =
|
|
36492
|
+
const traitCols = traitOnly;
|
|
35170
36493
|
throw new Error(
|
|
35171
36494
|
`Seed records set column(s) not defined on '${args.entity}': ${[...unknown2].join(", ")}. Its columns are: ${Object.keys(fields).join(", ") || "(none authored)"}` + (traitCols.length > 0 ? `, plus these from traits [${traits2.join(", ")}]: ${traitCols.join(", ")}.` : ".")
|
|
35172
36495
|
);
|
|
35173
36496
|
}
|
|
35174
|
-
for (const [fname, f] of Object.entries(
|
|
36497
|
+
for (const [fname, f] of Object.entries(allFields)) {
|
|
35175
36498
|
if (f?.columnType !== "R") continue;
|
|
35176
36499
|
const used = args.records.some((r) => r[fname] !== void 0);
|
|
35177
36500
|
if (used) {
|
|
@@ -35181,7 +36504,7 @@ function seedAdd(args) {
|
|
|
35181
36504
|
);
|
|
35182
36505
|
}
|
|
35183
36506
|
}
|
|
35184
|
-
for (const [fname, f] of Object.entries(
|
|
36507
|
+
for (const [fname, f] of Object.entries(allFields)) {
|
|
35185
36508
|
const flags = typeof f?.flags === "string" ? f.flags : "";
|
|
35186
36509
|
const isVirtual = f?.columnType === "R" || f?.columnType === "S" || f?.columnType === "F" || f?.columnType === "G";
|
|
35187
36510
|
if (isVirtual || !flags.includes("M") || f?.formula !== void 0) continue;
|
|
@@ -35706,9 +37029,17 @@ function moduleValidate(args) {
|
|
|
35706
37029
|
const err = (where, message) => issues.push({ level: "error", where, message });
|
|
35707
37030
|
const warn = (where, message) => issues.push({ level: "warning", where, message });
|
|
35708
37031
|
const entityMap = manifest.entities ?? {};
|
|
37032
|
+
const { traits: localTraits, error: traitsError } = readLocalTraits(paths.root);
|
|
37033
|
+
if (traitsError) {
|
|
37034
|
+
err(
|
|
37035
|
+
"traits.json",
|
|
37036
|
+
`${traitsError} This module's own traits can't be overlaid on the platform ones, so entities using them read as declaring an unknown trait \u2014 expect knock-on 'not a column' errors below.`
|
|
37037
|
+
);
|
|
37038
|
+
}
|
|
35709
37039
|
const entities = {};
|
|
35710
37040
|
const columnsOf = {};
|
|
35711
37041
|
const fieldDefsOf = {};
|
|
37042
|
+
const partialColumns = /* @__PURE__ */ new Set();
|
|
35712
37043
|
for (const [name, relPath] of Object.entries(entityMap)) {
|
|
35713
37044
|
if (name.includes(".")) continue;
|
|
35714
37045
|
const abs = path15.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
@@ -35724,20 +37055,19 @@ function moduleValidate(args) {
|
|
|
35724
37055
|
continue;
|
|
35725
37056
|
}
|
|
35726
37057
|
entities[name] = e;
|
|
35727
|
-
const fields = e.fields ?? {};
|
|
35728
|
-
const cols = new Set(Object.keys(fields));
|
|
35729
37058
|
const traits2 = e.traits ?? [];
|
|
35730
|
-
const badTraits = unknownTraits(traits2);
|
|
37059
|
+
const badTraits = unknownTraits(traits2, localTraits);
|
|
35731
37060
|
if (badTraits.length > 0) {
|
|
35732
37061
|
err(
|
|
35733
37062
|
`entities/${name}.json`,
|
|
35734
37063
|
`declares unknown trait(s): ${badTraits.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}. An unrecognized trait is ignored when columns are expanded, so its columns are missing from this entity \u2014 expect knock-on 'not a column' errors below.`
|
|
35735
37064
|
);
|
|
37065
|
+
partialColumns.add(name);
|
|
35736
37066
|
}
|
|
35737
|
-
const
|
|
35738
|
-
for (const c of
|
|
35739
|
-
columnsOf[name] =
|
|
35740
|
-
fieldDefsOf[name] =
|
|
37067
|
+
const { columns, fieldDefs, conflicts } = expandedEntity(e, name, localTraits);
|
|
37068
|
+
for (const c of conflicts) err(`entities/${name}.json`, describeTraitConflict(c));
|
|
37069
|
+
columnsOf[name] = columns;
|
|
37070
|
+
fieldDefsOf[name] = fieldDefs;
|
|
35741
37071
|
}
|
|
35742
37072
|
const deps = new Set(Object.keys(manifest.dependencies ?? {}));
|
|
35743
37073
|
const isKnownEntity = (code2) => {
|
|
@@ -36108,7 +37438,8 @@ function moduleValidate(args) {
|
|
|
36108
37438
|
err(where, `schedules action '${act}', which is not in ui/actions.json.`);
|
|
36109
37439
|
}
|
|
36110
37440
|
if (act && act in actions) {
|
|
36111
|
-
const
|
|
37441
|
+
const registered = actions[act] ?? {};
|
|
37442
|
+
const script = registered.script;
|
|
36112
37443
|
if (typeof script === "string") {
|
|
36113
37444
|
const dslPath = path15.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
36114
37445
|
if (fs12.existsSync(dslPath)) {
|
|
@@ -36117,8 +37448,13 @@ function moduleValidate(args) {
|
|
|
36117
37448
|
body = fs12.readFileSync(dslPath, "utf8");
|
|
36118
37449
|
} catch {
|
|
36119
37450
|
}
|
|
36120
|
-
for (const issue2 of
|
|
36121
|
-
|
|
37451
|
+
for (const issue2 of checkDsl2(body, {
|
|
37452
|
+
executionMode: registered.executionMode ?? registered.mode,
|
|
37453
|
+
viaJob: true,
|
|
37454
|
+
actionCode: act,
|
|
37455
|
+
moduleCode: manifest.code
|
|
37456
|
+
})) {
|
|
37457
|
+
if (issue2.rule !== "dsl/job-record-context") continue;
|
|
36122
37458
|
issues.push({ level: issue2.level, where, message: issue2.message });
|
|
36123
37459
|
}
|
|
36124
37460
|
}
|
|
@@ -36147,10 +37483,20 @@ function moduleValidate(args) {
|
|
|
36147
37483
|
continue;
|
|
36148
37484
|
}
|
|
36149
37485
|
const mode = a.executionMode ?? a.mode;
|
|
36150
|
-
|
|
37486
|
+
const entityCode = localEntityCode(
|
|
37487
|
+
a.entityCode,
|
|
37488
|
+
manifest.code
|
|
37489
|
+
);
|
|
37490
|
+
const columns = entityCode && !partialColumns.has(entityCode) ? columnsOf[entityCode] : void 0;
|
|
37491
|
+
for (const issue2 of checkDsl2(body, {
|
|
37492
|
+
executionMode: mode,
|
|
37493
|
+
actionCode: acode,
|
|
37494
|
+
moduleCode: manifest.code,
|
|
37495
|
+
entity: entityCode && columns ? { qualified: `${manifest.code}.${entityCode}`, columns } : void 0
|
|
37496
|
+
})) {
|
|
36151
37497
|
issues.push({
|
|
36152
37498
|
level: issue2.level,
|
|
36153
|
-
where: `logic/actions/${script}.dsl`,
|
|
37499
|
+
where: `logic/actions/${script}.dsl${issue2.line ? `:${issue2.line}` : ""}`,
|
|
36154
37500
|
message: `[${acode}] ${issue2.message}`
|
|
36155
37501
|
});
|
|
36156
37502
|
}
|
|
@@ -36756,7 +38102,7 @@ var resources = [
|
|
|
36756
38102
|
matrixExample("ui/data_views.json", "Canonical MATRIX data view: viewType 'matrix' + viewConfig with a dataset rowAxis (categories), a dropdown colAxis ('budget_line.quarter'), and an editable cell mapping rowKey/colKey/fields onto the budget_line entity."),
|
|
36757
38103
|
matrixExample("entities/budget_line.json", "Matrix CELL entity: one record per (category, quarter) \u2014 FK+Reference to the row-axis entity plus the dropdown column that is the col axis, with the editable 'amount' value column."),
|
|
36758
38104
|
matrixExample("entities/budget_category.json", "Matrix row-axis (dataset) entity."),
|
|
36759
|
-
matrixExample("manifest.json", "Matrix example manifest
|
|
38105
|
+
matrixExample("manifest.json", "Matrix example manifest: declares only 'entities' \u2014 the data view, roles and seed files are found by convention (ui/, security/, seed-data/), and the manifest schema is additionalProperties:false."),
|
|
36760
38106
|
matrixExample("security/roles.json", "Matrix example roles: S on the axis entity, SIUD on the editable cell entity."),
|
|
36761
38107
|
matrixExample("seed-data/01-categories.json", "Matrix example seed: row-axis category records."),
|
|
36762
38108
|
columnSecurityExample("entities/product.json", `Canonical ENTITY VIEWS (column-level security): one entity, two views \u2014 'storekeeper' (a plain column subset, prices absent entirely) and 'accountant' (with a per-column flags override making a column read-only in that view only, and a per-view formula override on an "F" column). Both list the primary key, which a view must.`),
|