@dforge-core/dforge-mcp 0.2.28 → 0.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/server.js +653 -243
- package/docs/creating-modules.md +1 -1
- package/package.json +46 -48
- package/resources/docs/conventions.md +2 -0
- package/resources/schemas/diagram.schema.json +101 -0
- package/resources/schemas/entity.schema.json +1 -1
- package/skills/dforge-mcp-author/SKILL.md +1 -1
- package/skills/dforge-module-build/SKILL.md +3 -1
package/dist/server.js
CHANGED
|
@@ -3226,8 +3226,8 @@ var require_utils = __commonJS({
|
|
|
3226
3226
|
}
|
|
3227
3227
|
return ind;
|
|
3228
3228
|
}
|
|
3229
|
-
function removeDotSegments(
|
|
3230
|
-
let input =
|
|
3229
|
+
function removeDotSegments(path21) {
|
|
3230
|
+
let input = path21;
|
|
3231
3231
|
const output = [];
|
|
3232
3232
|
let nextSlash = -1;
|
|
3233
3233
|
let len = 0;
|
|
@@ -3479,8 +3479,8 @@ var require_schemes = __commonJS({
|
|
|
3479
3479
|
wsComponent.secure = void 0;
|
|
3480
3480
|
}
|
|
3481
3481
|
if (wsComponent.resourceName) {
|
|
3482
|
-
const [
|
|
3483
|
-
wsComponent.path =
|
|
3482
|
+
const [path21, query] = wsComponent.resourceName.split("?");
|
|
3483
|
+
wsComponent.path = path21 && path21 !== "/" ? path21 : void 0;
|
|
3484
3484
|
wsComponent.query = query;
|
|
3485
3485
|
wsComponent.resourceName = void 0;
|
|
3486
3486
|
}
|
|
@@ -6899,12 +6899,12 @@ var require_dist = __commonJS({
|
|
|
6899
6899
|
throw new Error(`Unknown format "${name}"`);
|
|
6900
6900
|
return f;
|
|
6901
6901
|
};
|
|
6902
|
-
function addFormats(ajv, list,
|
|
6902
|
+
function addFormats(ajv, list, fs19, exportName) {
|
|
6903
6903
|
var _a3;
|
|
6904
6904
|
var _b;
|
|
6905
6905
|
(_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6906
6906
|
for (const f of list)
|
|
6907
|
-
ajv.addFormat(f,
|
|
6907
|
+
ajv.addFormat(f, fs19[f]);
|
|
6908
6908
|
}
|
|
6909
6909
|
module2.exports = exports2 = formatsPlugin;
|
|
6910
6910
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -7271,8 +7271,8 @@ function getErrorMap() {
|
|
|
7271
7271
|
|
|
7272
7272
|
// node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/parseUtil.js
|
|
7273
7273
|
var makeIssue = (params) => {
|
|
7274
|
-
const { data, path:
|
|
7275
|
-
const fullPath = [...
|
|
7274
|
+
const { data, path: path21, errorMaps, issueData } = params;
|
|
7275
|
+
const fullPath = [...path21, ...issueData.path || []];
|
|
7276
7276
|
const fullIssue = {
|
|
7277
7277
|
...issueData,
|
|
7278
7278
|
path: fullPath
|
|
@@ -7387,11 +7387,11 @@ var errorUtil;
|
|
|
7387
7387
|
|
|
7388
7388
|
// node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/types.js
|
|
7389
7389
|
var ParseInputLazyPath = class {
|
|
7390
|
-
constructor(parent, value,
|
|
7390
|
+
constructor(parent, value, path21, key) {
|
|
7391
7391
|
this._cachedPath = [];
|
|
7392
7392
|
this.parent = parent;
|
|
7393
7393
|
this.data = value;
|
|
7394
|
-
this._path =
|
|
7394
|
+
this._path = path21;
|
|
7395
7395
|
this._key = key;
|
|
7396
7396
|
}
|
|
7397
7397
|
get path() {
|
|
@@ -11311,10 +11311,10 @@ function mergeDefs(...defs) {
|
|
|
11311
11311
|
function cloneDef(schema2) {
|
|
11312
11312
|
return mergeDefs(schema2._zod.def);
|
|
11313
11313
|
}
|
|
11314
|
-
function getElementAtPath(obj,
|
|
11315
|
-
if (!
|
|
11314
|
+
function getElementAtPath(obj, path21) {
|
|
11315
|
+
if (!path21)
|
|
11316
11316
|
return obj;
|
|
11317
|
-
return
|
|
11317
|
+
return path21.reduce((acc, key) => acc?.[key], obj);
|
|
11318
11318
|
}
|
|
11319
11319
|
function promiseAllObject(promisesObj) {
|
|
11320
11320
|
const keys = Object.keys(promisesObj);
|
|
@@ -11723,11 +11723,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
11723
11723
|
}
|
|
11724
11724
|
return false;
|
|
11725
11725
|
}
|
|
11726
|
-
function prefixIssues(
|
|
11726
|
+
function prefixIssues(path21, issues) {
|
|
11727
11727
|
return issues.map((iss) => {
|
|
11728
11728
|
var _a3;
|
|
11729
11729
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
11730
|
-
iss.path.unshift(
|
|
11730
|
+
iss.path.unshift(path21);
|
|
11731
11731
|
return iss;
|
|
11732
11732
|
});
|
|
11733
11733
|
}
|
|
@@ -11874,16 +11874,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
11874
11874
|
}
|
|
11875
11875
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
11876
11876
|
const fieldErrors = { _errors: [] };
|
|
11877
|
-
const processError = (error52,
|
|
11877
|
+
const processError = (error52, path21 = []) => {
|
|
11878
11878
|
for (const issue2 of error52.issues) {
|
|
11879
11879
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
11880
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
11880
|
+
issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
|
|
11881
11881
|
} else if (issue2.code === "invalid_key") {
|
|
11882
|
-
processError({ issues: issue2.issues }, [...
|
|
11882
|
+
processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
|
|
11883
11883
|
} else if (issue2.code === "invalid_element") {
|
|
11884
|
-
processError({ issues: issue2.issues }, [...
|
|
11884
|
+
processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
|
|
11885
11885
|
} else {
|
|
11886
|
-
const fullpath = [...
|
|
11886
|
+
const fullpath = [...path21, ...issue2.path];
|
|
11887
11887
|
if (fullpath.length === 0) {
|
|
11888
11888
|
fieldErrors._errors.push(mapper(issue2));
|
|
11889
11889
|
} else {
|
|
@@ -11910,17 +11910,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
11910
11910
|
}
|
|
11911
11911
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
11912
11912
|
const result = { errors: [] };
|
|
11913
|
-
const processError = (error52,
|
|
11913
|
+
const processError = (error52, path21 = []) => {
|
|
11914
11914
|
var _a3, _b;
|
|
11915
11915
|
for (const issue2 of error52.issues) {
|
|
11916
11916
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
11917
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
11917
|
+
issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
|
|
11918
11918
|
} else if (issue2.code === "invalid_key") {
|
|
11919
|
-
processError({ issues: issue2.issues }, [...
|
|
11919
|
+
processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
|
|
11920
11920
|
} else if (issue2.code === "invalid_element") {
|
|
11921
|
-
processError({ issues: issue2.issues }, [...
|
|
11921
|
+
processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
|
|
11922
11922
|
} else {
|
|
11923
|
-
const fullpath = [...
|
|
11923
|
+
const fullpath = [...path21, ...issue2.path];
|
|
11924
11924
|
if (fullpath.length === 0) {
|
|
11925
11925
|
result.errors.push(mapper(issue2));
|
|
11926
11926
|
continue;
|
|
@@ -11952,8 +11952,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
11952
11952
|
}
|
|
11953
11953
|
function toDotPath(_path) {
|
|
11954
11954
|
const segs = [];
|
|
11955
|
-
const
|
|
11956
|
-
for (const seg of
|
|
11955
|
+
const path21 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
11956
|
+
for (const seg of path21) {
|
|
11957
11957
|
if (typeof seg === "number")
|
|
11958
11958
|
segs.push(`[${seg}]`);
|
|
11959
11959
|
else if (typeof seg === "symbol")
|
|
@@ -23065,11 +23065,11 @@ function normalizeObjectSchema(schema2) {
|
|
|
23065
23065
|
}
|
|
23066
23066
|
return void 0;
|
|
23067
23067
|
}
|
|
23068
|
-
function getDotPath(
|
|
23069
|
-
if (
|
|
23068
|
+
function getDotPath(path21) {
|
|
23069
|
+
if (path21.length === 0) {
|
|
23070
23070
|
return "object root";
|
|
23071
23071
|
}
|
|
23072
|
-
return
|
|
23072
|
+
return path21.reduce((acc, seg, index) => {
|
|
23073
23073
|
if (index === 0) {
|
|
23074
23074
|
return String(seg);
|
|
23075
23075
|
}
|
|
@@ -25094,13 +25094,13 @@ function resolveRef(ref, ctx) {
|
|
|
25094
25094
|
if (!ref.startsWith("#")) {
|
|
25095
25095
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
25096
25096
|
}
|
|
25097
|
-
const
|
|
25098
|
-
if (
|
|
25097
|
+
const path21 = ref.slice(1).split("/").filter(Boolean);
|
|
25098
|
+
if (path21.length === 0) {
|
|
25099
25099
|
return ctx.rootSchema;
|
|
25100
25100
|
}
|
|
25101
25101
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
25102
|
-
if (
|
|
25103
|
-
const key =
|
|
25102
|
+
if (path21[0] === defsKey) {
|
|
25103
|
+
const key = path21[1];
|
|
25104
25104
|
if (!key || !ctx.defs[key]) {
|
|
25105
25105
|
throw new Error(`Reference not found: ${ref}`);
|
|
25106
25106
|
}
|
|
@@ -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.23/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 = {
|
|
@@ -31201,6 +31201,7 @@ var SCHEMA_BINDINGS = [
|
|
|
31201
31201
|
{ fileMatch: ["entities/*.json"], schema: "entity" },
|
|
31202
31202
|
{ fileMatch: ["ui/data_views.json"], schema: "data-views" },
|
|
31203
31203
|
{ fileMatch: ["ui/folders.json"], schema: "folders" },
|
|
31204
|
+
{ fileMatch: ["docs/diagrams/*.json"], schema: "diagram" },
|
|
31204
31205
|
{ fileMatch: ["ui/menus.json"], schema: "menus" },
|
|
31205
31206
|
{ fileMatch: ["ui/reports.json"], schema: "reports" },
|
|
31206
31207
|
{ fileMatch: ["ui/print_templates.json"], schema: "print-templates" },
|
|
@@ -31283,7 +31284,7 @@ function buildZedTasks() {
|
|
|
31283
31284
|
var fs = __toESM(require("fs"));
|
|
31284
31285
|
var path = __toESM(require("path"));
|
|
31285
31286
|
|
|
31286
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31287
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/index.js
|
|
31287
31288
|
var AggType = {
|
|
31288
31289
|
Sum: "sum",
|
|
31289
31290
|
Count: "count",
|
|
@@ -31296,9 +31297,9 @@ var AGG_TYPE_LIST = Object.values(AggType);
|
|
|
31296
31297
|
var fieldTypes = [
|
|
31297
31298
|
{ cd: "text", name: "Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31298
31299
|
{ cd: "avatar", name: "Avatar Name", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31299
|
-
{ cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31300
|
-
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31301
|
-
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31300
|
+
{ cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" } },
|
|
31301
|
+
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^\\+?[0-9\\s\\-().]+$" } },
|
|
31302
|
+
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^https?://.+", patternFlags: "i" } },
|
|
31302
31303
|
{ cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31303
31304
|
{ cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31304
31305
|
{ cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
@@ -31466,19 +31467,19 @@ function addTraitField(sink, key, def) {
|
|
|
31466
31467
|
}
|
|
31467
31468
|
}
|
|
31468
31469
|
}
|
|
31469
|
-
function expandTraitInner(traitCd, entityName, localTraits,
|
|
31470
|
-
if (
|
|
31471
|
-
|
|
31470
|
+
function expandTraitInner(traitCd, entityName, localTraits, path21, sink) {
|
|
31471
|
+
if (path21.has(traitCd)) return;
|
|
31472
|
+
path21.add(traitCd);
|
|
31472
31473
|
const trait = localTraits && Object.hasOwn(localTraits, traitCd) ? localTraits[traitCd] : byCd3.get(traitCd);
|
|
31473
31474
|
if (trait) {
|
|
31474
31475
|
for (const included of trait.includes ?? []) {
|
|
31475
|
-
expandTraitInner(included, entityName, localTraits,
|
|
31476
|
+
expandTraitInner(included, entityName, localTraits, path21, sink);
|
|
31476
31477
|
}
|
|
31477
31478
|
for (const [name, def] of Object.entries(trait.fields ?? {})) {
|
|
31478
31479
|
addTraitField(sink, applyTemplate(name, entityName), templateField(def, entityName));
|
|
31479
31480
|
}
|
|
31480
31481
|
}
|
|
31481
|
-
|
|
31482
|
+
path21.delete(traitCd);
|
|
31482
31483
|
}
|
|
31483
31484
|
function expandTraits(traitCds, entityName, localTraits) {
|
|
31484
31485
|
const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map() };
|
|
@@ -31538,10 +31539,12 @@ function modulePaths(moduleDir) {
|
|
|
31538
31539
|
translationsDir: path.join(root, "translations"),
|
|
31539
31540
|
dataViews: path.join(root, "ui", "data_views.json"),
|
|
31540
31541
|
folders: path.join(root, "ui", "folders.json"),
|
|
31542
|
+
diagramsDir: path.join(root, "docs", "diagrams"),
|
|
31541
31543
|
menus: path.join(root, "ui", "menus.json"),
|
|
31542
31544
|
actions: path.join(root, "ui", "actions.json"),
|
|
31543
31545
|
reports: path.join(root, "ui", "reports.json"),
|
|
31544
31546
|
queries: path.join(root, "ui", "queries.json"),
|
|
31547
|
+
storedProcedures: path.join(root, "logic", "stored_procedures.json"),
|
|
31545
31548
|
printTemplates: path.join(root, "ui", "print_templates.json"),
|
|
31546
31549
|
roles: path.join(root, "security", "roles.json"),
|
|
31547
31550
|
jobs: path.join(root, "logic", "jobs.json"),
|
|
@@ -31569,6 +31572,52 @@ function readJsonOrDefault(absPath, dflt) {
|
|
|
31569
31572
|
throw new Error(`${absPath}: ${e.message}`);
|
|
31570
31573
|
}
|
|
31571
31574
|
}
|
|
31575
|
+
function diagramFiles(diagramsDir) {
|
|
31576
|
+
if (!fs.existsSync(diagramsDir)) return [];
|
|
31577
|
+
return fs.readdirSync(diagramsDir).filter((f) => f.endsWith(".json")).sort().map((f) => path.join(diagramsDir, f));
|
|
31578
|
+
}
|
|
31579
|
+
function isPlannedDiagramKey(key, entityMap) {
|
|
31580
|
+
return !key.includes(".") && !Object.prototype.hasOwnProperty.call(entityMap, key);
|
|
31581
|
+
}
|
|
31582
|
+
function likelyMisspelledEntity(key, entityMap) {
|
|
31583
|
+
let best;
|
|
31584
|
+
for (const code2 of Object.keys(entityMap)) {
|
|
31585
|
+
if (code2.includes(".")) continue;
|
|
31586
|
+
const d = editDistance(key, code2);
|
|
31587
|
+
if (d > 2 || d >= Math.min(key.length, code2.length) / 3) continue;
|
|
31588
|
+
if (!best || d < best.d) best = { code: code2, d };
|
|
31589
|
+
}
|
|
31590
|
+
return best?.code;
|
|
31591
|
+
}
|
|
31592
|
+
function editDistance(a, b) {
|
|
31593
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
31594
|
+
for (let i = 1; i <= a.length; i++) {
|
|
31595
|
+
const cur = [i];
|
|
31596
|
+
for (let j = 1; j <= b.length; j++) {
|
|
31597
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
31598
|
+
}
|
|
31599
|
+
prev = cur;
|
|
31600
|
+
}
|
|
31601
|
+
return prev[b.length];
|
|
31602
|
+
}
|
|
31603
|
+
function diagramKeyProblem(key, entityMap, deps, root) {
|
|
31604
|
+
if (Object.prototype.hasOwnProperty.call(entityMap, key)) return void 0;
|
|
31605
|
+
const dep = [...deps].filter((d) => key.startsWith(`${d}.`) && key.length > d.length + 1).sort((a, b) => b.length - a.length)[0];
|
|
31606
|
+
if (!dep) {
|
|
31607
|
+
return key.includes(".") ? `'${key}' is not an entity of this module, and no declared dependency matches its module prefix. Declare the dependency, or drop the entity from the diagram.` : `'${key}' is not an entity in manifest.entities. Drop it from the diagram, or fix the code.`;
|
|
31608
|
+
}
|
|
31609
|
+
const contractPath = path.join(root, "deps", `${dep}.json`);
|
|
31610
|
+
if (!fs.existsSync(contractPath)) return void 0;
|
|
31611
|
+
let consumed;
|
|
31612
|
+
try {
|
|
31613
|
+
consumed = JSON.parse(fs.readFileSync(contractPath, "utf8"))?.entities;
|
|
31614
|
+
} catch {
|
|
31615
|
+
return void 0;
|
|
31616
|
+
}
|
|
31617
|
+
if (!consumed || typeof consumed !== "object") return void 0;
|
|
31618
|
+
const entity = key.slice(dep.length + 1);
|
|
31619
|
+
return Object.prototype.hasOwnProperty.call(consumed, entity) ? void 0 : `'${entity}' is not among the entities deps/${dep}.json consumes from '${dep}'. Add it to the contract if the module uses it, or drop it from the diagram.`;
|
|
31620
|
+
}
|
|
31572
31621
|
function jsonText(obj) {
|
|
31573
31622
|
return JSON.stringify(obj, null, " ") + "\n";
|
|
31574
31623
|
}
|
|
@@ -34207,6 +34256,20 @@ function entityRename(args) {
|
|
|
34207
34256
|
const entityMap = manifest.entities ?? {};
|
|
34208
34257
|
if (!(oldE in entityMap)) throw new Error(`Entity '${oldE}' is not in the manifest.`);
|
|
34209
34258
|
if (newE in entityMap) throw new Error(`Entity '${newE}' already exists in this module.`);
|
|
34259
|
+
const collisions = diagramFiles(paths.diagramsDir).filter((fp) => {
|
|
34260
|
+
let d;
|
|
34261
|
+
try {
|
|
34262
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34263
|
+
} catch {
|
|
34264
|
+
return false;
|
|
34265
|
+
}
|
|
34266
|
+
return !!d && typeof d === "object" && diagramMentions(d, oldE) && diagramMentions(d, newE);
|
|
34267
|
+
}).map((fp) => `docs/diagrams/${path8.basename(fp)}`);
|
|
34268
|
+
if (collisions.length > 0) {
|
|
34269
|
+
throw new Error(
|
|
34270
|
+
`${collisions.join(", ")} already draw${collisions.length === 1 ? "s" : ""} a '${newE}' alongside '${oldE}' \u2014 renaming would merge the two nodes. Remove or rename '${newE}' in the diagram first.`
|
|
34271
|
+
);
|
|
34272
|
+
}
|
|
34210
34273
|
const files = {};
|
|
34211
34274
|
const deletes = [];
|
|
34212
34275
|
const changes = [];
|
|
@@ -34312,6 +34375,36 @@ function entityRename(args) {
|
|
|
34312
34375
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
34313
34376
|
changes.push("folders.json (entity bindings)");
|
|
34314
34377
|
}
|
|
34378
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
34379
|
+
let d;
|
|
34380
|
+
try {
|
|
34381
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34382
|
+
} catch {
|
|
34383
|
+
continue;
|
|
34384
|
+
}
|
|
34385
|
+
if (!d || typeof d !== "object") continue;
|
|
34386
|
+
const what = [];
|
|
34387
|
+
const ents = d.entities;
|
|
34388
|
+
if (ents && typeof ents === "object" && oldE in ents) {
|
|
34389
|
+
d.entities = renameKey(ents, oldE, newE);
|
|
34390
|
+
what.push("entity key");
|
|
34391
|
+
}
|
|
34392
|
+
let relTouched = false;
|
|
34393
|
+
for (const r of diagramRelations(d)) {
|
|
34394
|
+
if (r.from === oldE) {
|
|
34395
|
+
r.from = newE;
|
|
34396
|
+
relTouched = true;
|
|
34397
|
+
}
|
|
34398
|
+
if (r.to === oldE) {
|
|
34399
|
+
r.to = newE;
|
|
34400
|
+
relTouched = true;
|
|
34401
|
+
}
|
|
34402
|
+
}
|
|
34403
|
+
if (relTouched) what.push("relations");
|
|
34404
|
+
if (what.length === 0) continue;
|
|
34405
|
+
files[rel(paths.root, fp)] = jsonText(d);
|
|
34406
|
+
changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
|
|
34407
|
+
}
|
|
34315
34408
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
34316
34409
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
34317
34410
|
const fp = path8.join(paths.seedDataDir, file2);
|
|
@@ -34338,6 +34431,17 @@ function entityRename(args) {
|
|
|
34338
34431
|
deletes
|
|
34339
34432
|
);
|
|
34340
34433
|
}
|
|
34434
|
+
function diagramRelations(d) {
|
|
34435
|
+
if (!Array.isArray(d.relations)) return [];
|
|
34436
|
+
return d.relations.filter(
|
|
34437
|
+
(r) => !!r && typeof r === "object" && !Array.isArray(r)
|
|
34438
|
+
);
|
|
34439
|
+
}
|
|
34440
|
+
function diagramMentions(d, key) {
|
|
34441
|
+
const ents = d.entities;
|
|
34442
|
+
if (ents && typeof ents === "object" && Object.prototype.hasOwnProperty.call(ents, key)) return true;
|
|
34443
|
+
return diagramRelations(d).some((r) => r.from === key || r.to === key);
|
|
34444
|
+
}
|
|
34341
34445
|
var entityDeleteSchema = {
|
|
34342
34446
|
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
34343
34447
|
entityName: code.describe("Entity code to delete.")
|
|
@@ -34405,6 +34509,29 @@ function entityDelete(args) {
|
|
|
34405
34509
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
34406
34510
|
changes.push("folders.json (binding)");
|
|
34407
34511
|
}
|
|
34512
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
34513
|
+
let d;
|
|
34514
|
+
try {
|
|
34515
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34516
|
+
} catch {
|
|
34517
|
+
continue;
|
|
34518
|
+
}
|
|
34519
|
+
if (!d || typeof d !== "object") continue;
|
|
34520
|
+
const what = [];
|
|
34521
|
+
const ents = d.entities;
|
|
34522
|
+
if (ents && typeof ents === "object" && target in ents) {
|
|
34523
|
+
delete ents[target];
|
|
34524
|
+
what.push("entity key");
|
|
34525
|
+
}
|
|
34526
|
+
const doomed = new Set(diagramRelations(d).filter((r) => r.from === target || r.to === target));
|
|
34527
|
+
if (doomed.size > 0) {
|
|
34528
|
+
d.relations = d.relations.filter((r) => !doomed.has(r));
|
|
34529
|
+
what.push("relations");
|
|
34530
|
+
}
|
|
34531
|
+
if (what.length === 0) continue;
|
|
34532
|
+
files[rel(paths.root, fp)] = jsonText(d);
|
|
34533
|
+
changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
|
|
34534
|
+
}
|
|
34408
34535
|
const views = readJsonOrDefault(paths.dataViews, {});
|
|
34409
34536
|
let viewsTouched = false;
|
|
34410
34537
|
const removedViews = [];
|
|
@@ -34465,7 +34592,7 @@ function entityDelete(args) {
|
|
|
34465
34592
|
// src/tools/action-add.ts
|
|
34466
34593
|
var path9 = __toESM(require("path"));
|
|
34467
34594
|
|
|
34468
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34595
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34469
34596
|
var PUNCT3 = ["===", "!=="];
|
|
34470
34597
|
var PUNCT2 = [
|
|
34471
34598
|
"==",
|
|
@@ -36739,6 +36866,81 @@ function folderAdd(args) {
|
|
|
36739
36866
|
}
|
|
36740
36867
|
);
|
|
36741
36868
|
}
|
|
36869
|
+
var diagramAddSchema = {
|
|
36870
|
+
moduleDir: external_exports.string(),
|
|
36871
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Diagram code \u2014 becomes the file name docs/diagrams/<code>.json."),
|
|
36872
|
+
label: external_exports.string().optional().describe("Display name. Defaults to the fromFolder's label, else the code."),
|
|
36873
|
+
description: external_exports.string().optional().describe("What part of the model the diagram covers."),
|
|
36874
|
+
entities: external_exports.array(external_exports.string().min(1)).default([]).describe(
|
|
36875
|
+
"Entity keys to draw: the manifest's own entity code ('invoice'), or 'module.entity' for an entity of a declared dependency ('parties.party') \u2014 the same form a field link's `entity` takes."
|
|
36876
|
+
),
|
|
36877
|
+
fromFolder: external_exports.string().optional().describe(
|
|
36878
|
+
"A folder code from ui/folders.json (anywhere in the tree). Adds the entities bound to that folder AND to all its descendants."
|
|
36879
|
+
),
|
|
36880
|
+
showBoundary: external_exports.boolean().optional().describe(
|
|
36881
|
+
"Also draw the entities outside the diagram that its entities reference or are referenced by, as header-only stubs. Default true."
|
|
36882
|
+
)
|
|
36883
|
+
};
|
|
36884
|
+
function diagramAdd(args) {
|
|
36885
|
+
if (!/^[a-z][a-z0-9_]*$/.test(args.code)) {
|
|
36886
|
+
throw new Error(`Diagram code '${args.code}' must be snake_case (^[a-z][a-z0-9_]*$) \u2014 it becomes the file name.`);
|
|
36887
|
+
}
|
|
36888
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
36889
|
+
const entityMap = manifest.entities ?? {};
|
|
36890
|
+
const deps = new Set(Object.keys(manifest.dependencies ?? {}));
|
|
36891
|
+
const keys = [...args.entities];
|
|
36892
|
+
let folderLabel;
|
|
36893
|
+
if (args.fromFolder !== void 0) {
|
|
36894
|
+
const root = readJsonOrDefault(paths.folders, {});
|
|
36895
|
+
const folder = walkFolders(root).find((f) => f.code === args.fromFolder);
|
|
36896
|
+
if (!folder) {
|
|
36897
|
+
throw new Error(
|
|
36898
|
+
`Folder '${args.fromFolder}' is not in ui/folders.json. Pass a sub-folder's code (codes are unique across the tree); for every entity in the module, list them in 'entities' instead.`
|
|
36899
|
+
);
|
|
36900
|
+
}
|
|
36901
|
+
if (typeof folder.node.label === "string") folderLabel = folder.node.label;
|
|
36902
|
+
for (const node of [folder.node, ...walkFolders(folder.node).map((f) => f.node)]) {
|
|
36903
|
+
keys.push(...Object.keys(node.entities ?? {}));
|
|
36904
|
+
}
|
|
36905
|
+
}
|
|
36906
|
+
const wanted = [...new Set(keys)];
|
|
36907
|
+
const problems = wanted.map((k) => diagramKeyProblem(k, entityMap, deps, paths.root)).filter((p) => p !== void 0);
|
|
36908
|
+
if (problems.length > 0) {
|
|
36909
|
+
throw new Error(`Nothing written \u2014 ${problems.join(" ")}`);
|
|
36910
|
+
}
|
|
36911
|
+
const file2 = path13.join(paths.diagramsDir, `${args.code}.json`);
|
|
36912
|
+
const exists = fs10.existsSync(file2);
|
|
36913
|
+
let diagram;
|
|
36914
|
+
if (exists) {
|
|
36915
|
+
const existing = readJson(file2);
|
|
36916
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing) || !existing.entities || typeof existing.entities !== "object" || Array.isArray(existing.entities)) {
|
|
36917
|
+
throw new Error(
|
|
36918
|
+
`${rel(paths.root, file2)} is not a diagram (an object with an 'entities' map) \u2014 fix or delete it first.`
|
|
36919
|
+
);
|
|
36920
|
+
}
|
|
36921
|
+
diagram = existing;
|
|
36922
|
+
if (args.label !== void 0) diagram.label = args.label;
|
|
36923
|
+
if (args.description !== void 0) diagram.description = args.description;
|
|
36924
|
+
if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
|
|
36925
|
+
} else {
|
|
36926
|
+
diagram = { label: args.label ?? folderLabel ?? args.code };
|
|
36927
|
+
if (args.description !== void 0) diagram.description = args.description;
|
|
36928
|
+
diagram.entities = {};
|
|
36929
|
+
if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
|
|
36930
|
+
}
|
|
36931
|
+
const entities = diagram.entities;
|
|
36932
|
+
const added = wanted.filter((k) => !Object.prototype.hasOwnProperty.call(entities, k));
|
|
36933
|
+
for (const k of added) entities[k] = {};
|
|
36934
|
+
const relPath = rel(paths.root, file2);
|
|
36935
|
+
const present = wanted.length - added.length;
|
|
36936
|
+
return makeResult(
|
|
36937
|
+
exists ? `Updated diagram '${args.code}' (${relPath}): added ${added.length} entit${added.length === 1 ? "y" : "ies"}${present ? `, ${present} already drawn` : ""}.` : `Created diagram '${args.code}' (${relPath}) with ${added.length} entit${added.length === 1 ? "y" : "ies"}.`,
|
|
36938
|
+
{
|
|
36939
|
+
[relPath]: jsonText(diagram),
|
|
36940
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
36941
|
+
}
|
|
36942
|
+
);
|
|
36943
|
+
}
|
|
36742
36944
|
var SYSTEM_MODULES = /* @__PURE__ */ new Set(["admin", "metadata", "workspace"]);
|
|
36743
36945
|
var dependencyAddSchema = {
|
|
36744
36946
|
moduleDir: external_exports.string(),
|
|
@@ -36900,6 +37102,18 @@ function moduleInspect(args) {
|
|
|
36900
37102
|
}));
|
|
36901
37103
|
const foldersTree = readJsonOrDefault(paths.folders, {});
|
|
36902
37104
|
const folderDepth = computeDepth(foldersTree);
|
|
37105
|
+
const diagramSummaries = diagramFiles(paths.diagramsDir).map((fp) => {
|
|
37106
|
+
let d = {};
|
|
37107
|
+
try {
|
|
37108
|
+
d = JSON.parse(fs11.readFileSync(fp, "utf8"));
|
|
37109
|
+
} catch {
|
|
37110
|
+
}
|
|
37111
|
+
return {
|
|
37112
|
+
code: path14.basename(fp, ".json"),
|
|
37113
|
+
label: typeof d?.label === "string" ? d.label : void 0,
|
|
37114
|
+
entities: Object.keys(d?.entities ?? {})
|
|
37115
|
+
};
|
|
37116
|
+
});
|
|
36903
37117
|
const menus = readJsonOrDefault(paths.menus, {});
|
|
36904
37118
|
const menuSummaries = Object.entries(menus).map(([code2, m]) => ({
|
|
36905
37119
|
code: code2,
|
|
@@ -36967,6 +37181,7 @@ function moduleInspect(args) {
|
|
|
36967
37181
|
entities: entitySummaries,
|
|
36968
37182
|
views: viewSummaries,
|
|
36969
37183
|
folders: { tree: foldersTree, depth: folderDepth },
|
|
37184
|
+
diagrams: diagramSummaries,
|
|
36970
37185
|
menus: menuSummaries,
|
|
36971
37186
|
roles: roleSummaries,
|
|
36972
37187
|
actions: actionSummaries,
|
|
@@ -36996,52 +37211,17 @@ function computeDepth(folder, current = 1) {
|
|
|
36996
37211
|
return childDepths.length === 0 ? current : Math.max(...childDepths);
|
|
36997
37212
|
}
|
|
36998
37213
|
|
|
36999
|
-
// src/tools/
|
|
37214
|
+
// src/tools/validate/context.ts
|
|
37000
37215
|
var fs12 = __toESM(require("fs"));
|
|
37001
37216
|
var path15 = __toESM(require("path"));
|
|
37002
|
-
var moduleValidateSchema = {
|
|
37003
|
-
moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
|
|
37004
|
-
};
|
|
37005
37217
|
var SYSTEM_ENTITY_PK = {
|
|
37006
37218
|
user: "user_id",
|
|
37007
37219
|
document: "document_id",
|
|
37008
37220
|
menu_item: "menu_item_id",
|
|
37009
37221
|
resource: "resource_id"
|
|
37010
37222
|
};
|
|
37011
|
-
|
|
37012
|
-
|
|
37013
|
-
for (const f of Object.values(fields)) {
|
|
37014
|
-
if (!f || typeof f !== "object") continue;
|
|
37015
|
-
const flags = typeof f.flags === "string" ? f.flags : "";
|
|
37016
|
-
if (flags.includes("V") && f.columnType !== "S") return true;
|
|
37017
|
-
}
|
|
37018
|
-
return false;
|
|
37019
|
-
}
|
|
37020
|
-
function resolveTranslationFile(translationsDir, locale) {
|
|
37021
|
-
const exact = path15.join(translationsDir, `${locale}.json`);
|
|
37022
|
-
if (fs12.existsSync(exact)) return exact;
|
|
37023
|
-
if (!fs12.existsSync(translationsDir)) return void 0;
|
|
37024
|
-
const want = `${locale}.json`.toLowerCase();
|
|
37025
|
-
for (const f of fs12.readdirSync(translationsDir)) {
|
|
37026
|
-
if (f.toLowerCase() === want) return path15.join(translationsDir, f);
|
|
37027
|
-
}
|
|
37028
|
-
return void 0;
|
|
37029
|
-
}
|
|
37030
|
-
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
37031
|
-
if (!root || typeof root !== "object") return false;
|
|
37032
|
-
const entities = root.entities;
|
|
37033
|
-
if (!entities || typeof entities !== "object") return false;
|
|
37034
|
-
const entity = entities[entityCd];
|
|
37035
|
-
if (!entity || typeof entity !== "object") return false;
|
|
37036
|
-
const constraints = entity.constraints;
|
|
37037
|
-
if (!constraints || typeof constraints !== "object") return false;
|
|
37038
|
-
const ck = constraints[constraintCd];
|
|
37039
|
-
if (!ck || typeof ck !== "object") return false;
|
|
37040
|
-
const msg = ck.message;
|
|
37041
|
-
return typeof msg === "string" && msg.trim() !== "";
|
|
37042
|
-
}
|
|
37043
|
-
function moduleValidate(args, cli) {
|
|
37044
|
-
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
37223
|
+
function loadContext(moduleDir) {
|
|
37224
|
+
const { paths, manifest } = loadManifest(moduleDir);
|
|
37045
37225
|
const issues = [];
|
|
37046
37226
|
const err = (where, message) => issues.push({ level: "error", where, message });
|
|
37047
37227
|
const warn = (where, message) => issues.push({ level: "warning", where, message });
|
|
@@ -37102,6 +37282,35 @@ function moduleValidate(args, cli) {
|
|
|
37102
37282
|
if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
|
|
37103
37283
|
return void 0;
|
|
37104
37284
|
};
|
|
37285
|
+
const views = readJsonOrDefault(paths.dataViews, {});
|
|
37286
|
+
const supportedLocales = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
|
|
37287
|
+
return {
|
|
37288
|
+
moduleDir,
|
|
37289
|
+
paths,
|
|
37290
|
+
manifest,
|
|
37291
|
+
issues,
|
|
37292
|
+
err,
|
|
37293
|
+
warn,
|
|
37294
|
+
entityMap,
|
|
37295
|
+
entities,
|
|
37296
|
+
columnsOf,
|
|
37297
|
+
fieldDefsOf,
|
|
37298
|
+
partialColumns,
|
|
37299
|
+
deps,
|
|
37300
|
+
isKnownEntity,
|
|
37301
|
+
pkOf: pkOf2,
|
|
37302
|
+
views,
|
|
37303
|
+
viewCodes: new Set(Object.keys(views)),
|
|
37304
|
+
roles: readJsonOrDefault(paths.roles, {}),
|
|
37305
|
+
actions: readJsonOrDefault(paths.actions, {}),
|
|
37306
|
+
reports: readJsonOrDefault(paths.reports, {}),
|
|
37307
|
+
supportedLocales
|
|
37308
|
+
};
|
|
37309
|
+
}
|
|
37310
|
+
|
|
37311
|
+
// src/tools/validate/entities.ts
|
|
37312
|
+
function checkReferences(ctx) {
|
|
37313
|
+
const { err, warn, entities, columnsOf, isKnownEntity, pkOf: pkOf2 } = ctx;
|
|
37105
37314
|
for (const [name, e] of Object.entries(entities)) {
|
|
37106
37315
|
const fields = e.fields ?? {};
|
|
37107
37316
|
for (const [fname, f] of Object.entries(fields)) {
|
|
@@ -37133,6 +37342,9 @@ function moduleValidate(args, cli) {
|
|
|
37133
37342
|
}
|
|
37134
37343
|
}
|
|
37135
37344
|
}
|
|
37345
|
+
}
|
|
37346
|
+
function checkEntityViews(ctx) {
|
|
37347
|
+
const { err, entities, columnsOf, fieldDefsOf, pkOf: pkOf2 } = ctx;
|
|
37136
37348
|
for (const [name, e] of Object.entries(entities)) {
|
|
37137
37349
|
const entityViews = e.views ?? {};
|
|
37138
37350
|
if (Object.keys(entityViews).length === 0) continue;
|
|
@@ -37194,8 +37406,118 @@ function moduleValidate(args, cli) {
|
|
|
37194
37406
|
}
|
|
37195
37407
|
}
|
|
37196
37408
|
}
|
|
37197
|
-
|
|
37198
|
-
|
|
37409
|
+
}
|
|
37410
|
+
function checkFieldSpecs(ctx) {
|
|
37411
|
+
const { issues, entities } = ctx;
|
|
37412
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
37413
|
+
const fields = e.fields ?? {};
|
|
37414
|
+
for (const [fname, f] of Object.entries(fields)) {
|
|
37415
|
+
for (const issue2 of checkFieldSpec(`${name}.${fname}`, f)) {
|
|
37416
|
+
issues.push({ level: issue2.level, where: `entities/${name}.json`, message: issue2.message });
|
|
37417
|
+
}
|
|
37418
|
+
}
|
|
37419
|
+
}
|
|
37420
|
+
}
|
|
37421
|
+
function checkToString(ctx) {
|
|
37422
|
+
const { err, warn, entities, columnsOf } = ctx;
|
|
37423
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
37424
|
+
const isExtension = typeof e.extends === "string" && e.extends.length > 0;
|
|
37425
|
+
const ts = Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : void 0;
|
|
37426
|
+
if (ts === void 0 || ts === null || typeof ts === "string" && ts.trim() === "") {
|
|
37427
|
+
if (!isExtension) {
|
|
37428
|
+
warn(
|
|
37429
|
+
`entities/${name}.json`,
|
|
37430
|
+
`has no 'toString' template \u2014 every entity should have one, e.g. "toString": "{name}" (extension entities use null to inherit the base).`
|
|
37431
|
+
);
|
|
37432
|
+
}
|
|
37433
|
+
continue;
|
|
37434
|
+
}
|
|
37435
|
+
if (typeof ts !== "string") {
|
|
37436
|
+
err(`entities/${name}.json`, `'toString' must be a string template, got ${typeof ts}.`);
|
|
37437
|
+
continue;
|
|
37438
|
+
}
|
|
37439
|
+
const braces = [...ts.matchAll(/\{([a-z][a-z0-9_]*)\}/gi)].map((m) => m[1]);
|
|
37440
|
+
if (braces.length === 0) {
|
|
37441
|
+
warn(
|
|
37442
|
+
`entities/${name}.json`,
|
|
37443
|
+
`'toString' is "${ts}" with no {column} placeholder \u2014 every record will render the same label.`
|
|
37444
|
+
);
|
|
37445
|
+
}
|
|
37446
|
+
for (const b of braces) {
|
|
37447
|
+
if (!columnsOf[name].has(b)) {
|
|
37448
|
+
err(
|
|
37449
|
+
`entities/${name}.json`,
|
|
37450
|
+
`'toString' references {${b}}, which is not a column on '${name}'.`
|
|
37451
|
+
);
|
|
37452
|
+
}
|
|
37453
|
+
}
|
|
37454
|
+
}
|
|
37455
|
+
}
|
|
37456
|
+
function checkSetAggregates(ctx) {
|
|
37457
|
+
const { err, warn, entities, fieldDefsOf } = ctx;
|
|
37458
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
37459
|
+
const fields = e.fields ?? {};
|
|
37460
|
+
for (const [fname, f] of Object.entries(fields)) {
|
|
37461
|
+
const formula = typeof f?.formula === "string" ? f.formula : "";
|
|
37462
|
+
if (!formula) continue;
|
|
37463
|
+
const agg = parseSetAggregate(formula);
|
|
37464
|
+
if (!agg) continue;
|
|
37465
|
+
const where = `entities/${name}.json \u2192 ${fname}`;
|
|
37466
|
+
if (f.columnType === "F") {
|
|
37467
|
+
err(
|
|
37468
|
+
where,
|
|
37469
|
+
`is a Formula ('F') column with a set aggregate ${agg.agg}([${agg.setField}].[${agg.childField}]) \u2014 an F set-aggregate is unsupported and silently renders empty. Use a Generated ('G') column with dbDatatype + formula instead. (See dforge://reference/column-types.)`
|
|
37470
|
+
);
|
|
37471
|
+
continue;
|
|
37472
|
+
}
|
|
37473
|
+
if (f.columnType !== "G") continue;
|
|
37474
|
+
const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
|
|
37475
|
+
if (!setCol) {
|
|
37476
|
+
err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
|
|
37477
|
+
continue;
|
|
37478
|
+
}
|
|
37479
|
+
if (setCol.columnType !== "S") {
|
|
37480
|
+
warn(
|
|
37481
|
+
where,
|
|
37482
|
+
`aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
|
|
37483
|
+
);
|
|
37484
|
+
continue;
|
|
37485
|
+
}
|
|
37486
|
+
const childEntity = setCol.link?.entity;
|
|
37487
|
+
if (!childEntity) continue;
|
|
37488
|
+
const childDefs = fieldDefsOf[childEntity];
|
|
37489
|
+
if (!childDefs) continue;
|
|
37490
|
+
const childCol = childDefs[agg.childField];
|
|
37491
|
+
if (!childCol) {
|
|
37492
|
+
err(
|
|
37493
|
+
where,
|
|
37494
|
+
`aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
|
|
37495
|
+
);
|
|
37496
|
+
continue;
|
|
37497
|
+
}
|
|
37498
|
+
const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
|
|
37499
|
+
if (childType === "F" || childType === "R" || childType === "S") {
|
|
37500
|
+
err(
|
|
37501
|
+
where,
|
|
37502
|
+
`aggregates '[${agg.setField}].[${agg.childField}]', but '${childEntity}.${agg.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column \u2014 install fails with \`column old.${agg.childField} does not exist\`. Aggregate a 'D' (or same-row 'G') child column instead.`
|
|
37503
|
+
);
|
|
37504
|
+
}
|
|
37505
|
+
}
|
|
37506
|
+
}
|
|
37507
|
+
}
|
|
37508
|
+
|
|
37509
|
+
// src/tools/validate/views.ts
|
|
37510
|
+
var COLUMN_AGNOSTIC_VIEW_TYPES = /* @__PURE__ */ new Set(["diagram", "matrix", "library"]);
|
|
37511
|
+
function hasVisibleScalarColumn(fields) {
|
|
37512
|
+
for (const f of Object.values(fields)) {
|
|
37513
|
+
if (!f || typeof f !== "object") continue;
|
|
37514
|
+
const flags = typeof f.flags === "string" ? f.flags : "";
|
|
37515
|
+
if (flags.includes("V") && f.columnType !== "S") return true;
|
|
37516
|
+
}
|
|
37517
|
+
return false;
|
|
37518
|
+
}
|
|
37519
|
+
function checkDataViews(ctx) {
|
|
37520
|
+
const { err, columnsOf, isKnownEntity, views } = ctx;
|
|
37199
37521
|
for (const [vcode, v] of Object.entries(views)) {
|
|
37200
37522
|
const sources = v.dataSources ?? [];
|
|
37201
37523
|
for (const s of sources) {
|
|
@@ -37214,6 +37536,9 @@ function moduleValidate(args, cli) {
|
|
|
37214
37536
|
}
|
|
37215
37537
|
}
|
|
37216
37538
|
}
|
|
37539
|
+
}
|
|
37540
|
+
function checkVisibleColumns(ctx) {
|
|
37541
|
+
const { err, fieldDefsOf, views } = ctx;
|
|
37217
37542
|
const vcSeen = /* @__PURE__ */ new Set();
|
|
37218
37543
|
for (const [vcode, v] of Object.entries(views)) {
|
|
37219
37544
|
const sources = v.dataSources ?? [];
|
|
@@ -37235,6 +37560,9 @@ function moduleValidate(args, cli) {
|
|
|
37235
37560
|
);
|
|
37236
37561
|
}
|
|
37237
37562
|
}
|
|
37563
|
+
}
|
|
37564
|
+
function checkMenus(ctx) {
|
|
37565
|
+
const { paths, err, viewCodes } = ctx;
|
|
37238
37566
|
const menus = readJsonOrDefault(paths.menus, {});
|
|
37239
37567
|
const walk = (node, where) => {
|
|
37240
37568
|
if (!node || typeof node !== "object") return;
|
|
@@ -37248,9 +37576,31 @@ function moduleValidate(args, cli) {
|
|
|
37248
37576
|
}
|
|
37249
37577
|
};
|
|
37250
37578
|
for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
|
|
37251
|
-
|
|
37252
|
-
|
|
37253
|
-
|
|
37579
|
+
}
|
|
37580
|
+
|
|
37581
|
+
// src/tools/validate/security.ts
|
|
37582
|
+
var fs13 = __toESM(require("fs"));
|
|
37583
|
+
function checkRoleRights(ctx) {
|
|
37584
|
+
const { paths, manifest, err, deps, isKnownEntity, roles, actions, reports } = ctx;
|
|
37585
|
+
const storedProcedures = readJsonOrDefault(paths.storedProcedures, {});
|
|
37586
|
+
const checkObjectKey = (rcode, key, kind, own, absFile, file2) => {
|
|
37587
|
+
const ref = key.slice(kind.length + 1);
|
|
37588
|
+
const noun = kind === "sp" ? "stored procedure" : "report";
|
|
37589
|
+
if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/.test(ref)) {
|
|
37590
|
+
err(`roles \u2192 ${rcode}`, `malformed rights key '${key}' \u2014 expected '${kind}:code' or '${kind}:module.code', each part matching [a-z][a-z0-9_]*`);
|
|
37591
|
+
return;
|
|
37592
|
+
}
|
|
37593
|
+
const dot = ref.indexOf(".");
|
|
37594
|
+
const mod = dot < 0 ? manifest.code : ref.slice(0, dot);
|
|
37595
|
+
const code2 = dot < 0 ? ref : ref.slice(dot + 1);
|
|
37596
|
+
if (mod !== manifest.code) {
|
|
37597
|
+
if (!deps.has(mod)) err(`roles \u2192 ${rcode}`, `grants on '${key}' but '${mod}' is not a declared dependency`);
|
|
37598
|
+
} else if (!fs13.existsSync(absFile)) {
|
|
37599
|
+
err(`roles \u2192 ${rcode}`, `grants on '${key}' but ${file2} is missing \u2014 declare the ${noun} there`);
|
|
37600
|
+
} else if (!(code2 in own)) {
|
|
37601
|
+
err(`roles \u2192 ${rcode}`, `grants on '${key}' but no such ${noun} exists in ${file2}`);
|
|
37602
|
+
}
|
|
37603
|
+
};
|
|
37254
37604
|
for (const [rcode, r] of Object.entries(roles)) {
|
|
37255
37605
|
const rights = r.rights ?? {};
|
|
37256
37606
|
for (const key of Object.keys(rights)) {
|
|
@@ -37258,22 +37608,55 @@ function moduleValidate(args, cli) {
|
|
|
37258
37608
|
const a = key.slice("action:".length);
|
|
37259
37609
|
if (!(a in actions)) err(`roles \u2192 ${rcode}`, `grants on 'action:${a}' but no such action exists`);
|
|
37260
37610
|
} else if (key.startsWith("report:")) {
|
|
37261
|
-
|
|
37262
|
-
|
|
37611
|
+
checkObjectKey(rcode, key, "report", reports, paths.reports, "ui/reports.json");
|
|
37612
|
+
} else if (key.startsWith("sp:")) {
|
|
37613
|
+
checkObjectKey(rcode, key, "sp", storedProcedures, paths.storedProcedures, "logic/stored_procedures.json");
|
|
37263
37614
|
} else if (key.startsWith("folder:")) {
|
|
37264
37615
|
} else if (!isKnownEntity(key)) {
|
|
37265
37616
|
err(`roles \u2192 ${rcode}`, `grants rights on '${key}', which is not a known entity (same-module, system, or a declared cross-module dependency)`);
|
|
37266
37617
|
}
|
|
37267
37618
|
}
|
|
37268
37619
|
}
|
|
37620
|
+
}
|
|
37621
|
+
function checkSelectCoverage(ctx) {
|
|
37622
|
+
const { moduleDir, warn } = ctx;
|
|
37269
37623
|
try {
|
|
37270
|
-
const { uncoveredEntities } = checkSecurityCoverage(
|
|
37624
|
+
const { uncoveredEntities } = checkSecurityCoverage(moduleDir);
|
|
37271
37625
|
for (const e of uncoveredEntities) {
|
|
37272
37626
|
warn("security", `entity '${e}' has no role granting Select (S) \u2014 it will be inaccessible`);
|
|
37273
37627
|
}
|
|
37274
37628
|
} catch {
|
|
37275
37629
|
}
|
|
37276
|
-
|
|
37630
|
+
}
|
|
37631
|
+
|
|
37632
|
+
// src/tools/validate/translations.ts
|
|
37633
|
+
var fs14 = __toESM(require("fs"));
|
|
37634
|
+
var path16 = __toESM(require("path"));
|
|
37635
|
+
function resolveTranslationFile(translationsDir, locale) {
|
|
37636
|
+
const exact = path16.join(translationsDir, `${locale}.json`);
|
|
37637
|
+
if (fs14.existsSync(exact)) return exact;
|
|
37638
|
+
if (!fs14.existsSync(translationsDir)) return void 0;
|
|
37639
|
+
const want = `${locale}.json`.toLowerCase();
|
|
37640
|
+
for (const f of fs14.readdirSync(translationsDir)) {
|
|
37641
|
+
if (f.toLowerCase() === want) return path16.join(translationsDir, f);
|
|
37642
|
+
}
|
|
37643
|
+
return void 0;
|
|
37644
|
+
}
|
|
37645
|
+
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
37646
|
+
if (!root || typeof root !== "object") return false;
|
|
37647
|
+
const entities = root.entities;
|
|
37648
|
+
if (!entities || typeof entities !== "object") return false;
|
|
37649
|
+
const entity = entities[entityCd];
|
|
37650
|
+
if (!entity || typeof entity !== "object") return false;
|
|
37651
|
+
const constraints = entity.constraints;
|
|
37652
|
+
if (!constraints || typeof constraints !== "object") return false;
|
|
37653
|
+
const ck = constraints[constraintCd];
|
|
37654
|
+
if (!ck || typeof ck !== "object") return false;
|
|
37655
|
+
const msg = ck.message;
|
|
37656
|
+
return typeof msg === "string" && msg.trim() !== "";
|
|
37657
|
+
}
|
|
37658
|
+
function checkConstraintTranslations(ctx) {
|
|
37659
|
+
const { paths, warn, entities, supportedLocales } = ctx;
|
|
37277
37660
|
if (supportedLocales.length > 0) {
|
|
37278
37661
|
const declared = [];
|
|
37279
37662
|
for (const [name, e] of Object.entries(entities)) {
|
|
@@ -37301,7 +37684,7 @@ function moduleValidate(args, cli) {
|
|
|
37301
37684
|
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
37302
37685
|
if (abs) {
|
|
37303
37686
|
try {
|
|
37304
|
-
tx = JSON.parse(
|
|
37687
|
+
tx = JSON.parse(fs14.readFileSync(abs, "utf8"));
|
|
37305
37688
|
} catch {
|
|
37306
37689
|
tx = null;
|
|
37307
37690
|
}
|
|
@@ -37317,95 +37700,54 @@ function moduleValidate(args, cli) {
|
|
|
37317
37700
|
}
|
|
37318
37701
|
}
|
|
37319
37702
|
}
|
|
37320
|
-
|
|
37321
|
-
|
|
37322
|
-
|
|
37323
|
-
|
|
37324
|
-
|
|
37325
|
-
|
|
37703
|
+
}
|
|
37704
|
+
function checkTranslationCompleteness(ctx) {
|
|
37705
|
+
const { paths, err, warn, roles, supportedLocales } = ctx;
|
|
37706
|
+
const roleCodes = Object.keys(roles);
|
|
37707
|
+
const localeFiles = fs14.existsSync(paths.translationsDir) ? fs14.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
|
|
37708
|
+
for (const raw of supportedLocales) {
|
|
37709
|
+
const locale = raw.trim();
|
|
37710
|
+
if (!locale) continue;
|
|
37711
|
+
if (!resolveTranslationFile(paths.translationsDir, locale)) {
|
|
37712
|
+
err(
|
|
37713
|
+
"translations",
|
|
37714
|
+
`manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
|
|
37715
|
+
);
|
|
37326
37716
|
}
|
|
37327
37717
|
}
|
|
37328
|
-
|
|
37329
|
-
|
|
37330
|
-
|
|
37331
|
-
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
|
|
37338
|
-
|
|
37339
|
-
|
|
37340
|
-
if (typeof ts !== "string") {
|
|
37341
|
-
err(`entities/${name}.json`, `'toString' must be a string template, got ${typeof ts}.`);
|
|
37718
|
+
if (roleCodes.length > 0 && localeFiles.length === 0) {
|
|
37719
|
+
warn(
|
|
37720
|
+
"translations",
|
|
37721
|
+
`no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
|
|
37722
|
+
);
|
|
37723
|
+
}
|
|
37724
|
+
for (const file2 of localeFiles) {
|
|
37725
|
+
let tx;
|
|
37726
|
+
try {
|
|
37727
|
+
tx = JSON.parse(fs14.readFileSync(path16.join(paths.translationsDir, file2), "utf8"));
|
|
37728
|
+
} catch (ex) {
|
|
37729
|
+
err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
|
|
37342
37730
|
continue;
|
|
37343
37731
|
}
|
|
37344
|
-
const
|
|
37345
|
-
|
|
37346
|
-
|
|
37347
|
-
|
|
37348
|
-
|
|
37732
|
+
const txRoles = tx.roles ?? {};
|
|
37733
|
+
const missing = roleCodes.filter((rc) => {
|
|
37734
|
+
const entry = txRoles[rc];
|
|
37735
|
+
return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
|
|
37736
|
+
});
|
|
37737
|
+
if (missing.length > 0) {
|
|
37738
|
+
err(
|
|
37739
|
+
`translations/${file2}`,
|
|
37740
|
+
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
|
|
37349
37741
|
);
|
|
37350
37742
|
}
|
|
37351
|
-
for (const b of braces) {
|
|
37352
|
-
if (!columnsOf[name].has(b)) {
|
|
37353
|
-
err(
|
|
37354
|
-
`entities/${name}.json`,
|
|
37355
|
-
`'toString' references {${b}}, which is not a column on '${name}'.`
|
|
37356
|
-
);
|
|
37357
|
-
}
|
|
37358
|
-
}
|
|
37359
|
-
}
|
|
37360
|
-
for (const [name, e] of Object.entries(entities)) {
|
|
37361
|
-
const fields = e.fields ?? {};
|
|
37362
|
-
for (const [fname, f] of Object.entries(fields)) {
|
|
37363
|
-
const formula = typeof f?.formula === "string" ? f.formula : "";
|
|
37364
|
-
if (!formula) continue;
|
|
37365
|
-
const agg = parseSetAggregate(formula);
|
|
37366
|
-
if (!agg) continue;
|
|
37367
|
-
const where = `entities/${name}.json \u2192 ${fname}`;
|
|
37368
|
-
if (f.columnType === "F") {
|
|
37369
|
-
err(
|
|
37370
|
-
where,
|
|
37371
|
-
`is a Formula ('F') column with a set aggregate ${agg.agg}([${agg.setField}].[${agg.childField}]) \u2014 an F set-aggregate is unsupported and silently renders empty. Use a Generated ('G') column with dbDatatype + formula instead. (See dforge://reference/column-types.)`
|
|
37372
|
-
);
|
|
37373
|
-
continue;
|
|
37374
|
-
}
|
|
37375
|
-
if (f.columnType !== "G") continue;
|
|
37376
|
-
const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
|
|
37377
|
-
if (!setCol) {
|
|
37378
|
-
err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
|
|
37379
|
-
continue;
|
|
37380
|
-
}
|
|
37381
|
-
if (setCol.columnType !== "S") {
|
|
37382
|
-
warn(
|
|
37383
|
-
where,
|
|
37384
|
-
`aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
|
|
37385
|
-
);
|
|
37386
|
-
continue;
|
|
37387
|
-
}
|
|
37388
|
-
const childEntity = setCol.link?.entity;
|
|
37389
|
-
if (!childEntity) continue;
|
|
37390
|
-
const childDefs = fieldDefsOf[childEntity];
|
|
37391
|
-
if (!childDefs) continue;
|
|
37392
|
-
const childCol = childDefs[agg.childField];
|
|
37393
|
-
if (!childCol) {
|
|
37394
|
-
err(
|
|
37395
|
-
where,
|
|
37396
|
-
`aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
|
|
37397
|
-
);
|
|
37398
|
-
continue;
|
|
37399
|
-
}
|
|
37400
|
-
const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
|
|
37401
|
-
if (childType === "F" || childType === "R" || childType === "S") {
|
|
37402
|
-
err(
|
|
37403
|
-
where,
|
|
37404
|
-
`aggregates '[${agg.setField}].[${agg.childField}]', but '${childEntity}.${agg.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column \u2014 install fails with \`column old.${agg.childField} does not exist\`. Aggregate a 'D' (or same-row 'G') child column instead.`
|
|
37405
|
-
);
|
|
37406
|
-
}
|
|
37407
|
-
}
|
|
37408
37743
|
}
|
|
37744
|
+
}
|
|
37745
|
+
|
|
37746
|
+
// src/tools/validate/logic.ts
|
|
37747
|
+
var fs15 = __toESM(require("fs"));
|
|
37748
|
+
var path17 = __toESM(require("path"));
|
|
37749
|
+
function checkActions(ctx) {
|
|
37750
|
+
const { paths, err, isKnownEntity, actions } = ctx;
|
|
37409
37751
|
for (const [acode, a] of Object.entries(actions)) {
|
|
37410
37752
|
const act = a ?? {};
|
|
37411
37753
|
const where = `actions \u2192 ${acode}`;
|
|
@@ -37417,7 +37759,7 @@ function moduleValidate(args, cli) {
|
|
|
37417
37759
|
where,
|
|
37418
37760
|
`script '${script}' must be a BARE filename \u2014 no path, no '.dsl' extension (e.g. "script": "${script.replace(/\.dsl$/, "").split(/[\\/]/).pop()}").`
|
|
37419
37761
|
);
|
|
37420
|
-
} else if (!
|
|
37762
|
+
} else if (!fs15.existsSync(path17.join(paths.logicDir, "actions", `${script}.dsl`))) {
|
|
37421
37763
|
err(where, `script '${script}' has no file at logic/actions/${script}.dsl.`);
|
|
37422
37764
|
}
|
|
37423
37765
|
const ent = act.entityCode ?? act.entity;
|
|
@@ -37425,6 +37767,9 @@ function moduleValidate(args, cli) {
|
|
|
37425
37767
|
err(where, `targets entity '${ent}', which is not a known entity.`);
|
|
37426
37768
|
}
|
|
37427
37769
|
}
|
|
37770
|
+
}
|
|
37771
|
+
function checkTriggersJobsWebhooks(ctx) {
|
|
37772
|
+
const { paths, manifest, issues, err, deps, isKnownEntity, actions } = ctx;
|
|
37428
37773
|
const isKnownAction = (code2) => {
|
|
37429
37774
|
if (code2 in actions) return true;
|
|
37430
37775
|
const dot = code2.indexOf(".");
|
|
@@ -37458,11 +37803,11 @@ function moduleValidate(args, cli) {
|
|
|
37458
37803
|
const registered = actions[act] ?? {};
|
|
37459
37804
|
const script = registered.script;
|
|
37460
37805
|
if (typeof script === "string") {
|
|
37461
|
-
const dslPath =
|
|
37462
|
-
if (
|
|
37806
|
+
const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
37807
|
+
if (fs15.existsSync(dslPath)) {
|
|
37463
37808
|
let body = "";
|
|
37464
37809
|
try {
|
|
37465
|
-
body =
|
|
37810
|
+
body = fs15.readFileSync(dslPath, "utf8");
|
|
37466
37811
|
} catch {
|
|
37467
37812
|
}
|
|
37468
37813
|
for (const issue2 of checkDsl2(body, {
|
|
@@ -37488,14 +37833,17 @@ function moduleValidate(args, cli) {
|
|
|
37488
37833
|
err(`webhooks \u2192 ${String(w.code ?? "?")}`, `is bound to entity '${ent}', which is not a known entity.`);
|
|
37489
37834
|
}
|
|
37490
37835
|
}
|
|
37836
|
+
}
|
|
37837
|
+
function checkActionDsl(ctx) {
|
|
37838
|
+
const { paths, manifest, issues, columnsOf, partialColumns, actions } = ctx;
|
|
37491
37839
|
for (const [acode, a] of Object.entries(actions)) {
|
|
37492
37840
|
const script = a?.script;
|
|
37493
37841
|
if (typeof script !== "string" || !script) continue;
|
|
37494
|
-
const dslPath =
|
|
37495
|
-
if (!
|
|
37842
|
+
const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
37843
|
+
if (!fs15.existsSync(dslPath)) continue;
|
|
37496
37844
|
let body;
|
|
37497
37845
|
try {
|
|
37498
|
-
body =
|
|
37846
|
+
body = fs15.readFileSync(dslPath, "utf8");
|
|
37499
37847
|
} catch {
|
|
37500
37848
|
continue;
|
|
37501
37849
|
}
|
|
@@ -37518,6 +37866,13 @@ function moduleValidate(args, cli) {
|
|
|
37518
37866
|
});
|
|
37519
37867
|
}
|
|
37520
37868
|
}
|
|
37869
|
+
}
|
|
37870
|
+
|
|
37871
|
+
// src/tools/validate/folders.ts
|
|
37872
|
+
var fs16 = __toESM(require("fs"));
|
|
37873
|
+
var path18 = __toESM(require("path"));
|
|
37874
|
+
function checkFolders(ctx) {
|
|
37875
|
+
const { paths, err, entities } = ctx;
|
|
37521
37876
|
const folderRoot = readJsonOrDefault(paths.folders, {});
|
|
37522
37877
|
for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
|
|
37523
37878
|
err(
|
|
@@ -37549,6 +37904,55 @@ function moduleValidate(args, cli) {
|
|
|
37549
37904
|
}
|
|
37550
37905
|
}
|
|
37551
37906
|
}
|
|
37907
|
+
}
|
|
37908
|
+
function checkDiagrams(ctx) {
|
|
37909
|
+
const { paths, err, warn, entityMap, deps } = ctx;
|
|
37910
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
37911
|
+
const where = `docs/diagrams/${path18.basename(fp)}`;
|
|
37912
|
+
let diagram;
|
|
37913
|
+
try {
|
|
37914
|
+
diagram = JSON.parse(fs16.readFileSync(fp, "utf8"));
|
|
37915
|
+
} catch (ex) {
|
|
37916
|
+
err(where, `invalid JSON: ${ex.message}`);
|
|
37917
|
+
continue;
|
|
37918
|
+
}
|
|
37919
|
+
if (!diagram || typeof diagram !== "object" || Array.isArray(diagram)) {
|
|
37920
|
+
warn(where, "must be an object \u2014 the diagram itself, with an 'entities' map.");
|
|
37921
|
+
continue;
|
|
37922
|
+
}
|
|
37923
|
+
const diagramEntities = diagram.entities;
|
|
37924
|
+
if (!diagramEntities || typeof diagramEntities !== "object" || Array.isArray(diagramEntities)) {
|
|
37925
|
+
warn(where, "has no 'entities' object \u2014 list the entities the diagram draws, keyed by entity code.");
|
|
37926
|
+
continue;
|
|
37927
|
+
}
|
|
37928
|
+
for (const [key, placement] of Object.entries(diagramEntities)) {
|
|
37929
|
+
const at = `${where} \u2192 entities.${key}`;
|
|
37930
|
+
if (isPlannedDiagramKey(key, entityMap)) {
|
|
37931
|
+
const meant = likelyMisspelledEntity(key, entityMap);
|
|
37932
|
+
if (meant) {
|
|
37933
|
+
warn(
|
|
37934
|
+
at,
|
|
37935
|
+
`'${key}' is not built yet, so the diagram draws it as a planned entity. If you meant the existing '${meant}', fix the key.`
|
|
37936
|
+
);
|
|
37937
|
+
}
|
|
37938
|
+
} else {
|
|
37939
|
+
const unresolved = diagramKeyProblem(key, entityMap, deps, paths.root);
|
|
37940
|
+
if (unresolved) warn(at, unresolved);
|
|
37941
|
+
}
|
|
37942
|
+
const p = placement && typeof placement === "object" ? placement : {};
|
|
37943
|
+
if ("x" in p !== "y" in p) {
|
|
37944
|
+
warn(
|
|
37945
|
+
at,
|
|
37946
|
+
`sets '${"x" in p ? "x" : "y"}' without '${"x" in p ? "y" : "x"}'. Give both to pin the entity, or neither to let the diagram place it.`
|
|
37947
|
+
);
|
|
37948
|
+
}
|
|
37949
|
+
}
|
|
37950
|
+
}
|
|
37951
|
+
}
|
|
37952
|
+
|
|
37953
|
+
// src/tools/validate/reports.ts
|
|
37954
|
+
function checkReports(ctx) {
|
|
37955
|
+
const { manifest, err, warn, fieldDefsOf, deps, isKnownEntity, reports } = ctx;
|
|
37552
37956
|
for (const [rcode, rawReport] of Object.entries(reports)) {
|
|
37553
37957
|
const report = rawReport ?? {};
|
|
37554
37958
|
const where = `ui/reports.json \u2192 ${rcode}`;
|
|
@@ -37662,44 +38066,36 @@ function moduleValidate(args, cli) {
|
|
|
37662
38066
|
);
|
|
37663
38067
|
}
|
|
37664
38068
|
}
|
|
37665
|
-
|
|
37666
|
-
|
|
37667
|
-
|
|
37668
|
-
|
|
37669
|
-
|
|
37670
|
-
|
|
37671
|
-
|
|
37672
|
-
|
|
37673
|
-
|
|
37674
|
-
|
|
37675
|
-
|
|
37676
|
-
|
|
37677
|
-
|
|
37678
|
-
|
|
37679
|
-
|
|
37680
|
-
|
|
37681
|
-
|
|
37682
|
-
|
|
37683
|
-
|
|
37684
|
-
|
|
37685
|
-
|
|
37686
|
-
|
|
37687
|
-
|
|
37688
|
-
|
|
37689
|
-
|
|
37690
|
-
|
|
37691
|
-
|
|
37692
|
-
|
|
37693
|
-
|
|
37694
|
-
|
|
37695
|
-
});
|
|
37696
|
-
if (missing.length > 0) {
|
|
37697
|
-
err(
|
|
37698
|
-
`translations/${file2}`,
|
|
37699
|
-
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
|
|
37700
|
-
);
|
|
37701
|
-
}
|
|
37702
|
-
}
|
|
38069
|
+
}
|
|
38070
|
+
|
|
38071
|
+
// src/tools/module-validate.ts
|
|
38072
|
+
var moduleValidateSchema = {
|
|
38073
|
+
moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
|
|
38074
|
+
};
|
|
38075
|
+
var CHECKS = [
|
|
38076
|
+
checkReferences,
|
|
38077
|
+
checkEntityViews,
|
|
38078
|
+
checkDataViews,
|
|
38079
|
+
checkVisibleColumns,
|
|
38080
|
+
checkMenus,
|
|
38081
|
+
checkRoleRights,
|
|
38082
|
+
checkSelectCoverage,
|
|
38083
|
+
checkConstraintTranslations,
|
|
38084
|
+
checkFieldSpecs,
|
|
38085
|
+
checkToString,
|
|
38086
|
+
checkSetAggregates,
|
|
38087
|
+
checkActions,
|
|
38088
|
+
checkTriggersJobsWebhooks,
|
|
38089
|
+
checkActionDsl,
|
|
38090
|
+
checkFolders,
|
|
38091
|
+
checkDiagrams,
|
|
38092
|
+
checkReports,
|
|
38093
|
+
checkTranslationCompleteness
|
|
38094
|
+
];
|
|
38095
|
+
function moduleValidate(args, cli) {
|
|
38096
|
+
const ctx = loadContext(args.moduleDir);
|
|
38097
|
+
for (const check2 of CHECKS) check2(ctx);
|
|
38098
|
+
const { manifest, issues, err, warn, entities, viewCodes, roles } = ctx;
|
|
37703
38099
|
if (cli) {
|
|
37704
38100
|
const res = cli(args.moduleDir);
|
|
37705
38101
|
if ("unavailable" in res) {
|
|
@@ -37881,18 +38277,18 @@ function webhookAdd(args) {
|
|
|
37881
38277
|
}
|
|
37882
38278
|
|
|
37883
38279
|
// src/tools/apply.ts
|
|
37884
|
-
var
|
|
37885
|
-
var
|
|
38280
|
+
var fs17 = __toESM(require("fs"));
|
|
38281
|
+
var path19 = __toESM(require("path"));
|
|
37886
38282
|
var applyInput = {
|
|
37887
38283
|
apply: external_exports.boolean().default(false).describe(
|
|
37888
38284
|
"When true, WRITE the resulting files to disk (and remove any `deletes`) instead of returning their contents for the client to write. The response then lists the paths touched. Use it for routine patches the user isn't reviewing line-by-line; leave it false when you want to preview a diff first."
|
|
37889
38285
|
)
|
|
37890
38286
|
};
|
|
37891
|
-
var isReportPayload = (relPath) =>
|
|
38287
|
+
var isReportPayload = (relPath) => path19.basename(relPath).startsWith("_");
|
|
37892
38288
|
function resolveInside(root, relPath) {
|
|
37893
|
-
const abs =
|
|
37894
|
-
const rel2 =
|
|
37895
|
-
if (rel2.startsWith("..") ||
|
|
38289
|
+
const abs = path19.resolve(root, relPath);
|
|
38290
|
+
const rel2 = path19.relative(root, abs);
|
|
38291
|
+
if (rel2.startsWith("..") || path19.isAbsolute(rel2)) {
|
|
37896
38292
|
throw new Error(
|
|
37897
38293
|
`Refusing to write '${relPath}' \u2014 it resolves outside the module directory (${abs}).`
|
|
37898
38294
|
);
|
|
@@ -37900,7 +38296,7 @@ function resolveInside(root, relPath) {
|
|
|
37900
38296
|
return abs;
|
|
37901
38297
|
}
|
|
37902
38298
|
function applyToDisk(moduleDir, result) {
|
|
37903
|
-
const root =
|
|
38299
|
+
const root = path19.resolve(moduleDir);
|
|
37904
38300
|
const written = [];
|
|
37905
38301
|
const skipped = [];
|
|
37906
38302
|
for (const [relPath, contents] of Object.entries(result.files ?? {})) {
|
|
@@ -37909,8 +38305,8 @@ function applyToDisk(moduleDir, result) {
|
|
|
37909
38305
|
continue;
|
|
37910
38306
|
}
|
|
37911
38307
|
const abs = resolveInside(root, relPath);
|
|
37912
|
-
|
|
37913
|
-
|
|
38308
|
+
fs17.mkdirSync(path19.dirname(abs), { recursive: true });
|
|
38309
|
+
fs17.writeFileSync(abs, contents, "utf8");
|
|
37914
38310
|
written.push(relPath);
|
|
37915
38311
|
}
|
|
37916
38312
|
const deleted = [];
|
|
@@ -37918,7 +38314,7 @@ function applyToDisk(moduleDir, result) {
|
|
|
37918
38314
|
const abs = resolveInside(root, relPath);
|
|
37919
38315
|
let stat;
|
|
37920
38316
|
try {
|
|
37921
|
-
stat =
|
|
38317
|
+
stat = fs17.lstatSync(abs);
|
|
37922
38318
|
} catch {
|
|
37923
38319
|
continue;
|
|
37924
38320
|
}
|
|
@@ -37927,7 +38323,7 @@ function applyToDisk(moduleDir, result) {
|
|
|
37927
38323
|
`Refusing to delete '${relPath}': it is a directory, and \`deletes\` is a file-only contract (it exists to remove a renamed entity's old JSON or an orphaned seed file). Remove the directory by hand if that is really what you want.`
|
|
37928
38324
|
);
|
|
37929
38325
|
}
|
|
37930
|
-
|
|
38326
|
+
fs17.rmSync(abs, { force: true });
|
|
37931
38327
|
deleted.push(relPath);
|
|
37932
38328
|
}
|
|
37933
38329
|
const out = {
|
|
@@ -37942,10 +38338,10 @@ function applyToDisk(moduleDir, result) {
|
|
|
37942
38338
|
}
|
|
37943
38339
|
|
|
37944
38340
|
// src/resources/index.ts
|
|
37945
|
-
var
|
|
37946
|
-
var
|
|
37947
|
-
var RESOURCES_DIR =
|
|
37948
|
-
var SKILL_DIR =
|
|
38341
|
+
var fs18 = __toESM(require("fs"));
|
|
38342
|
+
var path20 = __toESM(require("path"));
|
|
38343
|
+
var RESOURCES_DIR = path20.resolve(__dirname, "..", "resources");
|
|
38344
|
+
var SKILL_DIR = path20.resolve(__dirname, "..", "skills", "dforge-mcp-author");
|
|
37949
38345
|
function schema(name, label, description) {
|
|
37950
38346
|
return {
|
|
37951
38347
|
uri: `dforge://schema/${name}`,
|
|
@@ -38022,6 +38418,11 @@ var resources = [
|
|
|
38022
38418
|
"Folders JSON schema",
|
|
38023
38419
|
"JSON Schema for ui/folders.json \u2014 root folder tree with per-entity view bindings."
|
|
38024
38420
|
),
|
|
38421
|
+
schema(
|
|
38422
|
+
"diagram",
|
|
38423
|
+
"Diagram JSON schema",
|
|
38424
|
+
"JSON Schema for docs/diagrams/<code>.json \u2014 one entity diagram per file, the file name is its code (a subset of entities + optional x/y placement each). Design-time only: install ignores it."
|
|
38425
|
+
),
|
|
38025
38426
|
schema(
|
|
38026
38427
|
"menus",
|
|
38027
38428
|
"Menus JSON schema",
|
|
@@ -38148,20 +38549,20 @@ var resources = [
|
|
|
38148
38549
|
columnSecurityExample("README.md", "Why entity views exist, what each of the two views demonstrates, and the five ways a view fails the install.")
|
|
38149
38550
|
];
|
|
38150
38551
|
function readVendored(rel2) {
|
|
38151
|
-
const p =
|
|
38152
|
-
if (!
|
|
38552
|
+
const p = path20.join(RESOURCES_DIR, rel2);
|
|
38553
|
+
if (!fs18.existsSync(p)) {
|
|
38153
38554
|
return `// Resource missing at build time: ${rel2}
|
|
38154
38555
|
// Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
|
|
38155
38556
|
}
|
|
38156
|
-
return
|
|
38557
|
+
return fs18.readFileSync(p, "utf8");
|
|
38157
38558
|
}
|
|
38158
38559
|
function readSkill(rel2) {
|
|
38159
|
-
const p =
|
|
38160
|
-
if (!
|
|
38560
|
+
const p = path20.join(SKILL_DIR, rel2);
|
|
38561
|
+
if (!fs18.existsSync(p)) {
|
|
38161
38562
|
return `// Skill resource missing: ${rel2}
|
|
38162
38563
|
// Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
|
|
38163
38564
|
}
|
|
38164
|
-
return
|
|
38565
|
+
return fs18.readFileSync(p, "utf8");
|
|
38165
38566
|
}
|
|
38166
38567
|
|
|
38167
38568
|
// src/server.ts
|
|
@@ -38309,7 +38710,7 @@ server.registerTool(
|
|
|
38309
38710
|
"dforge_entity_rename",
|
|
38310
38711
|
{
|
|
38311
38712
|
title: "Rename an entity (refactor-safe)",
|
|
38312
|
-
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38713
|
+
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, diagram entity keys and relation endpoints (docs/diagrams/*.json \u2014 refused if a diagram already draws the new code next to the old), and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38313
38714
|
inputSchema: { ...entityRenameSchema, ...applyInput },
|
|
38314
38715
|
annotations: DESTRUCTIVE
|
|
38315
38716
|
},
|
|
@@ -38319,7 +38720,7 @@ server.registerTool(
|
|
|
38319
38720
|
"dforge_entity_delete",
|
|
38320
38721
|
{
|
|
38321
38722
|
title: "Delete an entity (refactor-safe)",
|
|
38322
|
-
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38723
|
+
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, diagram entries and the relations touching it (docs/diagrams/*.json), and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38323
38724
|
inputSchema: { ...entityDeleteSchema, ...applyInput },
|
|
38324
38725
|
annotations: DESTRUCTIVE
|
|
38325
38726
|
},
|
|
@@ -38534,6 +38935,15 @@ server.registerTool(
|
|
|
38534
38935
|
},
|
|
38535
38936
|
envelope(dependencyAdd)
|
|
38536
38937
|
);
|
|
38938
|
+
server.registerTool(
|
|
38939
|
+
"dforge_diagram_add",
|
|
38940
|
+
{
|
|
38941
|
+
title: "Add or extend an entity diagram",
|
|
38942
|
+
description: "Create or extend a named entity diagram, docs/diagrams/<code>.json \u2014 one file per diagram, drawing a chosen subset of entities. Design-time only (install ignores it). Use it on a LARGE module to give each functional area its own readable diagram (e.g. 'billing', 'inventory'); a small module needs none. Pass `entities` (own code, or 'module.entity' for a declared dependency) and/or `fromFolder` to take a folder's entities plus all its descendants'. On an existing diagram it only appends missing entities \u2014 positions already set are kept and nothing is removed. Leave layout to the editor: new entities get no x/y. An entity key that resolves to nothing is rejected and nothing is written.",
|
|
38943
|
+
inputSchema: { ...diagramAddSchema, ...applyInput }
|
|
38944
|
+
},
|
|
38945
|
+
envelope(diagramAdd)
|
|
38946
|
+
);
|
|
38537
38947
|
server.registerTool(
|
|
38538
38948
|
"dforge_dbml_import",
|
|
38539
38949
|
{
|