@dforge-core/dforge-mcp 0.2.29 → 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/dist/server.js +382 -240
- package/package.json +46 -48
- package/resources/docs/conventions.md +2 -0
- 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" },
|
|
@@ -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() };
|
|
@@ -31543,6 +31544,7 @@ function modulePaths(moduleDir) {
|
|
|
31543
31544
|
actions: path.join(root, "ui", "actions.json"),
|
|
31544
31545
|
reports: path.join(root, "ui", "reports.json"),
|
|
31545
31546
|
queries: path.join(root, "ui", "queries.json"),
|
|
31547
|
+
storedProcedures: path.join(root, "logic", "stored_procedures.json"),
|
|
31546
31548
|
printTemplates: path.join(root, "ui", "print_templates.json"),
|
|
31547
31549
|
roles: path.join(root, "security", "roles.json"),
|
|
31548
31550
|
jobs: path.join(root, "logic", "jobs.json"),
|
|
@@ -37209,52 +37211,17 @@ function computeDepth(folder, current = 1) {
|
|
|
37209
37211
|
return childDepths.length === 0 ? current : Math.max(...childDepths);
|
|
37210
37212
|
}
|
|
37211
37213
|
|
|
37212
|
-
// src/tools/
|
|
37214
|
+
// src/tools/validate/context.ts
|
|
37213
37215
|
var fs12 = __toESM(require("fs"));
|
|
37214
37216
|
var path15 = __toESM(require("path"));
|
|
37215
|
-
var moduleValidateSchema = {
|
|
37216
|
-
moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
|
|
37217
|
-
};
|
|
37218
37217
|
var SYSTEM_ENTITY_PK = {
|
|
37219
37218
|
user: "user_id",
|
|
37220
37219
|
document: "document_id",
|
|
37221
37220
|
menu_item: "menu_item_id",
|
|
37222
37221
|
resource: "resource_id"
|
|
37223
37222
|
};
|
|
37224
|
-
|
|
37225
|
-
|
|
37226
|
-
for (const f of Object.values(fields)) {
|
|
37227
|
-
if (!f || typeof f !== "object") continue;
|
|
37228
|
-
const flags = typeof f.flags === "string" ? f.flags : "";
|
|
37229
|
-
if (flags.includes("V") && f.columnType !== "S") return true;
|
|
37230
|
-
}
|
|
37231
|
-
return false;
|
|
37232
|
-
}
|
|
37233
|
-
function resolveTranslationFile(translationsDir, locale) {
|
|
37234
|
-
const exact = path15.join(translationsDir, `${locale}.json`);
|
|
37235
|
-
if (fs12.existsSync(exact)) return exact;
|
|
37236
|
-
if (!fs12.existsSync(translationsDir)) return void 0;
|
|
37237
|
-
const want = `${locale}.json`.toLowerCase();
|
|
37238
|
-
for (const f of fs12.readdirSync(translationsDir)) {
|
|
37239
|
-
if (f.toLowerCase() === want) return path15.join(translationsDir, f);
|
|
37240
|
-
}
|
|
37241
|
-
return void 0;
|
|
37242
|
-
}
|
|
37243
|
-
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
37244
|
-
if (!root || typeof root !== "object") return false;
|
|
37245
|
-
const entities = root.entities;
|
|
37246
|
-
if (!entities || typeof entities !== "object") return false;
|
|
37247
|
-
const entity = entities[entityCd];
|
|
37248
|
-
if (!entity || typeof entity !== "object") return false;
|
|
37249
|
-
const constraints = entity.constraints;
|
|
37250
|
-
if (!constraints || typeof constraints !== "object") return false;
|
|
37251
|
-
const ck = constraints[constraintCd];
|
|
37252
|
-
if (!ck || typeof ck !== "object") return false;
|
|
37253
|
-
const msg = ck.message;
|
|
37254
|
-
return typeof msg === "string" && msg.trim() !== "";
|
|
37255
|
-
}
|
|
37256
|
-
function moduleValidate(args, cli) {
|
|
37257
|
-
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
37223
|
+
function loadContext(moduleDir) {
|
|
37224
|
+
const { paths, manifest } = loadManifest(moduleDir);
|
|
37258
37225
|
const issues = [];
|
|
37259
37226
|
const err = (where, message) => issues.push({ level: "error", where, message });
|
|
37260
37227
|
const warn = (where, message) => issues.push({ level: "warning", where, message });
|
|
@@ -37315,6 +37282,35 @@ function moduleValidate(args, cli) {
|
|
|
37315
37282
|
if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
|
|
37316
37283
|
return void 0;
|
|
37317
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;
|
|
37318
37314
|
for (const [name, e] of Object.entries(entities)) {
|
|
37319
37315
|
const fields = e.fields ?? {};
|
|
37320
37316
|
for (const [fname, f] of Object.entries(fields)) {
|
|
@@ -37346,6 +37342,9 @@ function moduleValidate(args, cli) {
|
|
|
37346
37342
|
}
|
|
37347
37343
|
}
|
|
37348
37344
|
}
|
|
37345
|
+
}
|
|
37346
|
+
function checkEntityViews(ctx) {
|
|
37347
|
+
const { err, entities, columnsOf, fieldDefsOf, pkOf: pkOf2 } = ctx;
|
|
37349
37348
|
for (const [name, e] of Object.entries(entities)) {
|
|
37350
37349
|
const entityViews = e.views ?? {};
|
|
37351
37350
|
if (Object.keys(entityViews).length === 0) continue;
|
|
@@ -37407,8 +37406,118 @@ function moduleValidate(args, cli) {
|
|
|
37407
37406
|
}
|
|
37408
37407
|
}
|
|
37409
37408
|
}
|
|
37410
|
-
|
|
37411
|
-
|
|
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;
|
|
37412
37521
|
for (const [vcode, v] of Object.entries(views)) {
|
|
37413
37522
|
const sources = v.dataSources ?? [];
|
|
37414
37523
|
for (const s of sources) {
|
|
@@ -37427,6 +37536,9 @@ function moduleValidate(args, cli) {
|
|
|
37427
37536
|
}
|
|
37428
37537
|
}
|
|
37429
37538
|
}
|
|
37539
|
+
}
|
|
37540
|
+
function checkVisibleColumns(ctx) {
|
|
37541
|
+
const { err, fieldDefsOf, views } = ctx;
|
|
37430
37542
|
const vcSeen = /* @__PURE__ */ new Set();
|
|
37431
37543
|
for (const [vcode, v] of Object.entries(views)) {
|
|
37432
37544
|
const sources = v.dataSources ?? [];
|
|
@@ -37448,6 +37560,9 @@ function moduleValidate(args, cli) {
|
|
|
37448
37560
|
);
|
|
37449
37561
|
}
|
|
37450
37562
|
}
|
|
37563
|
+
}
|
|
37564
|
+
function checkMenus(ctx) {
|
|
37565
|
+
const { paths, err, viewCodes } = ctx;
|
|
37451
37566
|
const menus = readJsonOrDefault(paths.menus, {});
|
|
37452
37567
|
const walk = (node, where) => {
|
|
37453
37568
|
if (!node || typeof node !== "object") return;
|
|
@@ -37461,9 +37576,31 @@ function moduleValidate(args, cli) {
|
|
|
37461
37576
|
}
|
|
37462
37577
|
};
|
|
37463
37578
|
for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
|
|
37464
|
-
|
|
37465
|
-
|
|
37466
|
-
|
|
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
|
+
};
|
|
37467
37604
|
for (const [rcode, r] of Object.entries(roles)) {
|
|
37468
37605
|
const rights = r.rights ?? {};
|
|
37469
37606
|
for (const key of Object.keys(rights)) {
|
|
@@ -37471,22 +37608,55 @@ function moduleValidate(args, cli) {
|
|
|
37471
37608
|
const a = key.slice("action:".length);
|
|
37472
37609
|
if (!(a in actions)) err(`roles \u2192 ${rcode}`, `grants on 'action:${a}' but no such action exists`);
|
|
37473
37610
|
} else if (key.startsWith("report:")) {
|
|
37474
|
-
|
|
37475
|
-
|
|
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");
|
|
37476
37614
|
} else if (key.startsWith("folder:")) {
|
|
37477
37615
|
} else if (!isKnownEntity(key)) {
|
|
37478
37616
|
err(`roles \u2192 ${rcode}`, `grants rights on '${key}', which is not a known entity (same-module, system, or a declared cross-module dependency)`);
|
|
37479
37617
|
}
|
|
37480
37618
|
}
|
|
37481
37619
|
}
|
|
37620
|
+
}
|
|
37621
|
+
function checkSelectCoverage(ctx) {
|
|
37622
|
+
const { moduleDir, warn } = ctx;
|
|
37482
37623
|
try {
|
|
37483
|
-
const { uncoveredEntities } = checkSecurityCoverage(
|
|
37624
|
+
const { uncoveredEntities } = checkSecurityCoverage(moduleDir);
|
|
37484
37625
|
for (const e of uncoveredEntities) {
|
|
37485
37626
|
warn("security", `entity '${e}' has no role granting Select (S) \u2014 it will be inaccessible`);
|
|
37486
37627
|
}
|
|
37487
37628
|
} catch {
|
|
37488
37629
|
}
|
|
37489
|
-
|
|
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;
|
|
37490
37660
|
if (supportedLocales.length > 0) {
|
|
37491
37661
|
const declared = [];
|
|
37492
37662
|
for (const [name, e] of Object.entries(entities)) {
|
|
@@ -37514,7 +37684,7 @@ function moduleValidate(args, cli) {
|
|
|
37514
37684
|
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
37515
37685
|
if (abs) {
|
|
37516
37686
|
try {
|
|
37517
|
-
tx = JSON.parse(
|
|
37687
|
+
tx = JSON.parse(fs14.readFileSync(abs, "utf8"));
|
|
37518
37688
|
} catch {
|
|
37519
37689
|
tx = null;
|
|
37520
37690
|
}
|
|
@@ -37530,95 +37700,54 @@ function moduleValidate(args, cli) {
|
|
|
37530
37700
|
}
|
|
37531
37701
|
}
|
|
37532
37702
|
}
|
|
37533
|
-
|
|
37534
|
-
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
|
|
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
|
+
);
|
|
37539
37716
|
}
|
|
37540
37717
|
}
|
|
37541
|
-
|
|
37542
|
-
|
|
37543
|
-
|
|
37544
|
-
|
|
37545
|
-
|
|
37546
|
-
|
|
37547
|
-
|
|
37548
|
-
|
|
37549
|
-
|
|
37550
|
-
|
|
37551
|
-
|
|
37552
|
-
|
|
37553
|
-
if (typeof ts !== "string") {
|
|
37554
|
-
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}`);
|
|
37555
37730
|
continue;
|
|
37556
37731
|
}
|
|
37557
|
-
const
|
|
37558
|
-
|
|
37559
|
-
|
|
37560
|
-
|
|
37561
|
-
|
|
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>'."`
|
|
37562
37741
|
);
|
|
37563
37742
|
}
|
|
37564
|
-
for (const b of braces) {
|
|
37565
|
-
if (!columnsOf[name].has(b)) {
|
|
37566
|
-
err(
|
|
37567
|
-
`entities/${name}.json`,
|
|
37568
|
-
`'toString' references {${b}}, which is not a column on '${name}'.`
|
|
37569
|
-
);
|
|
37570
|
-
}
|
|
37571
|
-
}
|
|
37572
|
-
}
|
|
37573
|
-
for (const [name, e] of Object.entries(entities)) {
|
|
37574
|
-
const fields = e.fields ?? {};
|
|
37575
|
-
for (const [fname, f] of Object.entries(fields)) {
|
|
37576
|
-
const formula = typeof f?.formula === "string" ? f.formula : "";
|
|
37577
|
-
if (!formula) continue;
|
|
37578
|
-
const agg = parseSetAggregate(formula);
|
|
37579
|
-
if (!agg) continue;
|
|
37580
|
-
const where = `entities/${name}.json \u2192 ${fname}`;
|
|
37581
|
-
if (f.columnType === "F") {
|
|
37582
|
-
err(
|
|
37583
|
-
where,
|
|
37584
|
-
`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.)`
|
|
37585
|
-
);
|
|
37586
|
-
continue;
|
|
37587
|
-
}
|
|
37588
|
-
if (f.columnType !== "G") continue;
|
|
37589
|
-
const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
|
|
37590
|
-
if (!setCol) {
|
|
37591
|
-
err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
|
|
37592
|
-
continue;
|
|
37593
|
-
}
|
|
37594
|
-
if (setCol.columnType !== "S") {
|
|
37595
|
-
warn(
|
|
37596
|
-
where,
|
|
37597
|
-
`aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
|
|
37598
|
-
);
|
|
37599
|
-
continue;
|
|
37600
|
-
}
|
|
37601
|
-
const childEntity = setCol.link?.entity;
|
|
37602
|
-
if (!childEntity) continue;
|
|
37603
|
-
const childDefs = fieldDefsOf[childEntity];
|
|
37604
|
-
if (!childDefs) continue;
|
|
37605
|
-
const childCol = childDefs[agg.childField];
|
|
37606
|
-
if (!childCol) {
|
|
37607
|
-
err(
|
|
37608
|
-
where,
|
|
37609
|
-
`aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
|
|
37610
|
-
);
|
|
37611
|
-
continue;
|
|
37612
|
-
}
|
|
37613
|
-
const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
|
|
37614
|
-
if (childType === "F" || childType === "R" || childType === "S") {
|
|
37615
|
-
err(
|
|
37616
|
-
where,
|
|
37617
|
-
`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.`
|
|
37618
|
-
);
|
|
37619
|
-
}
|
|
37620
|
-
}
|
|
37621
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;
|
|
37622
37751
|
for (const [acode, a] of Object.entries(actions)) {
|
|
37623
37752
|
const act = a ?? {};
|
|
37624
37753
|
const where = `actions \u2192 ${acode}`;
|
|
@@ -37630,7 +37759,7 @@ function moduleValidate(args, cli) {
|
|
|
37630
37759
|
where,
|
|
37631
37760
|
`script '${script}' must be a BARE filename \u2014 no path, no '.dsl' extension (e.g. "script": "${script.replace(/\.dsl$/, "").split(/[\\/]/).pop()}").`
|
|
37632
37761
|
);
|
|
37633
|
-
} else if (!
|
|
37762
|
+
} else if (!fs15.existsSync(path17.join(paths.logicDir, "actions", `${script}.dsl`))) {
|
|
37634
37763
|
err(where, `script '${script}' has no file at logic/actions/${script}.dsl.`);
|
|
37635
37764
|
}
|
|
37636
37765
|
const ent = act.entityCode ?? act.entity;
|
|
@@ -37638,6 +37767,9 @@ function moduleValidate(args, cli) {
|
|
|
37638
37767
|
err(where, `targets entity '${ent}', which is not a known entity.`);
|
|
37639
37768
|
}
|
|
37640
37769
|
}
|
|
37770
|
+
}
|
|
37771
|
+
function checkTriggersJobsWebhooks(ctx) {
|
|
37772
|
+
const { paths, manifest, issues, err, deps, isKnownEntity, actions } = ctx;
|
|
37641
37773
|
const isKnownAction = (code2) => {
|
|
37642
37774
|
if (code2 in actions) return true;
|
|
37643
37775
|
const dot = code2.indexOf(".");
|
|
@@ -37671,11 +37803,11 @@ function moduleValidate(args, cli) {
|
|
|
37671
37803
|
const registered = actions[act] ?? {};
|
|
37672
37804
|
const script = registered.script;
|
|
37673
37805
|
if (typeof script === "string") {
|
|
37674
|
-
const dslPath =
|
|
37675
|
-
if (
|
|
37806
|
+
const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
37807
|
+
if (fs15.existsSync(dslPath)) {
|
|
37676
37808
|
let body = "";
|
|
37677
37809
|
try {
|
|
37678
|
-
body =
|
|
37810
|
+
body = fs15.readFileSync(dslPath, "utf8");
|
|
37679
37811
|
} catch {
|
|
37680
37812
|
}
|
|
37681
37813
|
for (const issue2 of checkDsl2(body, {
|
|
@@ -37701,14 +37833,17 @@ function moduleValidate(args, cli) {
|
|
|
37701
37833
|
err(`webhooks \u2192 ${String(w.code ?? "?")}`, `is bound to entity '${ent}', which is not a known entity.`);
|
|
37702
37834
|
}
|
|
37703
37835
|
}
|
|
37836
|
+
}
|
|
37837
|
+
function checkActionDsl(ctx) {
|
|
37838
|
+
const { paths, manifest, issues, columnsOf, partialColumns, actions } = ctx;
|
|
37704
37839
|
for (const [acode, a] of Object.entries(actions)) {
|
|
37705
37840
|
const script = a?.script;
|
|
37706
37841
|
if (typeof script !== "string" || !script) continue;
|
|
37707
|
-
const dslPath =
|
|
37708
|
-
if (!
|
|
37842
|
+
const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
37843
|
+
if (!fs15.existsSync(dslPath)) continue;
|
|
37709
37844
|
let body;
|
|
37710
37845
|
try {
|
|
37711
|
-
body =
|
|
37846
|
+
body = fs15.readFileSync(dslPath, "utf8");
|
|
37712
37847
|
} catch {
|
|
37713
37848
|
continue;
|
|
37714
37849
|
}
|
|
@@ -37731,6 +37866,13 @@ function moduleValidate(args, cli) {
|
|
|
37731
37866
|
});
|
|
37732
37867
|
}
|
|
37733
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;
|
|
37734
37876
|
const folderRoot = readJsonOrDefault(paths.folders, {});
|
|
37735
37877
|
for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
|
|
37736
37878
|
err(
|
|
@@ -37762,11 +37904,14 @@ function moduleValidate(args, cli) {
|
|
|
37762
37904
|
}
|
|
37763
37905
|
}
|
|
37764
37906
|
}
|
|
37907
|
+
}
|
|
37908
|
+
function checkDiagrams(ctx) {
|
|
37909
|
+
const { paths, err, warn, entityMap, deps } = ctx;
|
|
37765
37910
|
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
37766
|
-
const where = `docs/diagrams/${
|
|
37911
|
+
const where = `docs/diagrams/${path18.basename(fp)}`;
|
|
37767
37912
|
let diagram;
|
|
37768
37913
|
try {
|
|
37769
|
-
diagram = JSON.parse(
|
|
37914
|
+
diagram = JSON.parse(fs16.readFileSync(fp, "utf8"));
|
|
37770
37915
|
} catch (ex) {
|
|
37771
37916
|
err(where, `invalid JSON: ${ex.message}`);
|
|
37772
37917
|
continue;
|
|
@@ -37803,6 +37948,11 @@ function moduleValidate(args, cli) {
|
|
|
37803
37948
|
}
|
|
37804
37949
|
}
|
|
37805
37950
|
}
|
|
37951
|
+
}
|
|
37952
|
+
|
|
37953
|
+
// src/tools/validate/reports.ts
|
|
37954
|
+
function checkReports(ctx) {
|
|
37955
|
+
const { manifest, err, warn, fieldDefsOf, deps, isKnownEntity, reports } = ctx;
|
|
37806
37956
|
for (const [rcode, rawReport] of Object.entries(reports)) {
|
|
37807
37957
|
const report = rawReport ?? {};
|
|
37808
37958
|
const where = `ui/reports.json \u2192 ${rcode}`;
|
|
@@ -37916,44 +38066,36 @@ function moduleValidate(args, cli) {
|
|
|
37916
38066
|
);
|
|
37917
38067
|
}
|
|
37918
38068
|
}
|
|
37919
|
-
|
|
37920
|
-
|
|
37921
|
-
|
|
37922
|
-
|
|
37923
|
-
|
|
37924
|
-
|
|
37925
|
-
|
|
37926
|
-
|
|
37927
|
-
|
|
37928
|
-
|
|
37929
|
-
|
|
37930
|
-
|
|
37931
|
-
|
|
37932
|
-
|
|
37933
|
-
|
|
37934
|
-
|
|
37935
|
-
|
|
37936
|
-
|
|
37937
|
-
|
|
37938
|
-
|
|
37939
|
-
|
|
37940
|
-
|
|
37941
|
-
|
|
37942
|
-
|
|
37943
|
-
|
|
37944
|
-
|
|
37945
|
-
|
|
37946
|
-
|
|
37947
|
-
|
|
37948
|
-
|
|
37949
|
-
});
|
|
37950
|
-
if (missing.length > 0) {
|
|
37951
|
-
err(
|
|
37952
|
-
`translations/${file2}`,
|
|
37953
|
-
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
|
|
37954
|
-
);
|
|
37955
|
-
}
|
|
37956
|
-
}
|
|
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;
|
|
37957
38099
|
if (cli) {
|
|
37958
38100
|
const res = cli(args.moduleDir);
|
|
37959
38101
|
if ("unavailable" in res) {
|
|
@@ -38135,18 +38277,18 @@ function webhookAdd(args) {
|
|
|
38135
38277
|
}
|
|
38136
38278
|
|
|
38137
38279
|
// src/tools/apply.ts
|
|
38138
|
-
var
|
|
38139
|
-
var
|
|
38280
|
+
var fs17 = __toESM(require("fs"));
|
|
38281
|
+
var path19 = __toESM(require("path"));
|
|
38140
38282
|
var applyInput = {
|
|
38141
38283
|
apply: external_exports.boolean().default(false).describe(
|
|
38142
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."
|
|
38143
38285
|
)
|
|
38144
38286
|
};
|
|
38145
|
-
var isReportPayload = (relPath) =>
|
|
38287
|
+
var isReportPayload = (relPath) => path19.basename(relPath).startsWith("_");
|
|
38146
38288
|
function resolveInside(root, relPath) {
|
|
38147
|
-
const abs =
|
|
38148
|
-
const rel2 =
|
|
38149
|
-
if (rel2.startsWith("..") ||
|
|
38289
|
+
const abs = path19.resolve(root, relPath);
|
|
38290
|
+
const rel2 = path19.relative(root, abs);
|
|
38291
|
+
if (rel2.startsWith("..") || path19.isAbsolute(rel2)) {
|
|
38150
38292
|
throw new Error(
|
|
38151
38293
|
`Refusing to write '${relPath}' \u2014 it resolves outside the module directory (${abs}).`
|
|
38152
38294
|
);
|
|
@@ -38154,7 +38296,7 @@ function resolveInside(root, relPath) {
|
|
|
38154
38296
|
return abs;
|
|
38155
38297
|
}
|
|
38156
38298
|
function applyToDisk(moduleDir, result) {
|
|
38157
|
-
const root =
|
|
38299
|
+
const root = path19.resolve(moduleDir);
|
|
38158
38300
|
const written = [];
|
|
38159
38301
|
const skipped = [];
|
|
38160
38302
|
for (const [relPath, contents] of Object.entries(result.files ?? {})) {
|
|
@@ -38163,8 +38305,8 @@ function applyToDisk(moduleDir, result) {
|
|
|
38163
38305
|
continue;
|
|
38164
38306
|
}
|
|
38165
38307
|
const abs = resolveInside(root, relPath);
|
|
38166
|
-
|
|
38167
|
-
|
|
38308
|
+
fs17.mkdirSync(path19.dirname(abs), { recursive: true });
|
|
38309
|
+
fs17.writeFileSync(abs, contents, "utf8");
|
|
38168
38310
|
written.push(relPath);
|
|
38169
38311
|
}
|
|
38170
38312
|
const deleted = [];
|
|
@@ -38172,7 +38314,7 @@ function applyToDisk(moduleDir, result) {
|
|
|
38172
38314
|
const abs = resolveInside(root, relPath);
|
|
38173
38315
|
let stat;
|
|
38174
38316
|
try {
|
|
38175
|
-
stat =
|
|
38317
|
+
stat = fs17.lstatSync(abs);
|
|
38176
38318
|
} catch {
|
|
38177
38319
|
continue;
|
|
38178
38320
|
}
|
|
@@ -38181,7 +38323,7 @@ function applyToDisk(moduleDir, result) {
|
|
|
38181
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.`
|
|
38182
38324
|
);
|
|
38183
38325
|
}
|
|
38184
|
-
|
|
38326
|
+
fs17.rmSync(abs, { force: true });
|
|
38185
38327
|
deleted.push(relPath);
|
|
38186
38328
|
}
|
|
38187
38329
|
const out = {
|
|
@@ -38196,10 +38338,10 @@ function applyToDisk(moduleDir, result) {
|
|
|
38196
38338
|
}
|
|
38197
38339
|
|
|
38198
38340
|
// src/resources/index.ts
|
|
38199
|
-
var
|
|
38200
|
-
var
|
|
38201
|
-
var RESOURCES_DIR =
|
|
38202
|
-
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");
|
|
38203
38345
|
function schema(name, label, description) {
|
|
38204
38346
|
return {
|
|
38205
38347
|
uri: `dforge://schema/${name}`,
|
|
@@ -38407,20 +38549,20 @@ var resources = [
|
|
|
38407
38549
|
columnSecurityExample("README.md", "Why entity views exist, what each of the two views demonstrates, and the five ways a view fails the install.")
|
|
38408
38550
|
];
|
|
38409
38551
|
function readVendored(rel2) {
|
|
38410
|
-
const p =
|
|
38411
|
-
if (!
|
|
38552
|
+
const p = path20.join(RESOURCES_DIR, rel2);
|
|
38553
|
+
if (!fs18.existsSync(p)) {
|
|
38412
38554
|
return `// Resource missing at build time: ${rel2}
|
|
38413
38555
|
// Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
|
|
38414
38556
|
}
|
|
38415
|
-
return
|
|
38557
|
+
return fs18.readFileSync(p, "utf8");
|
|
38416
38558
|
}
|
|
38417
38559
|
function readSkill(rel2) {
|
|
38418
|
-
const p =
|
|
38419
|
-
if (!
|
|
38560
|
+
const p = path20.join(SKILL_DIR, rel2);
|
|
38561
|
+
if (!fs18.existsSync(p)) {
|
|
38420
38562
|
return `// Skill resource missing: ${rel2}
|
|
38421
38563
|
// Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
|
|
38422
38564
|
}
|
|
38423
|
-
return
|
|
38565
|
+
return fs18.readFileSync(p, "utf8");
|
|
38424
38566
|
}
|
|
38425
38567
|
|
|
38426
38568
|
// src/server.ts
|
|
@@ -38568,7 +38710,7 @@ server.registerTool(
|
|
|
38568
38710
|
"dforge_entity_rename",
|
|
38569
38711
|
{
|
|
38570
38712
|
title: "Rename an entity (refactor-safe)",
|
|
38571
|
-
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 (docs/diagrams/*.json), 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.",
|
|
38572
38714
|
inputSchema: { ...entityRenameSchema, ...applyInput },
|
|
38573
38715
|
annotations: DESTRUCTIVE
|
|
38574
38716
|
},
|
|
@@ -38578,7 +38720,7 @@ server.registerTool(
|
|
|
38578
38720
|
"dforge_entity_delete",
|
|
38579
38721
|
{
|
|
38580
38722
|
title: "Delete an entity (refactor-safe)",
|
|
38581
|
-
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 (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.",
|
|
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.",
|
|
38582
38724
|
inputSchema: { ...entityDeleteSchema, ...applyInput },
|
|
38583
38725
|
annotations: DESTRUCTIVE
|
|
38584
38726
|
},
|
package/package.json
CHANGED
|
@@ -1,49 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
}
|
|
2
|
+
"name": "@dforge-core/dforge-mcp",
|
|
3
|
+
"version": "0.2.30",
|
|
4
|
+
"description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/iash44/dForge-core",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/dforge-core/dforge-mcp.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"dforge-mcp": "dist/server.js",
|
|
13
|
+
"dforge-install-skills": "scripts/install-skills.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"docs/",
|
|
18
|
+
"resources/",
|
|
19
|
+
"skills/",
|
|
20
|
+
"scripts/install-skills.mjs",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"main": "dist/server.js",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@dforge-core/dforge-cli": "^0.2.23",
|
|
30
|
+
"@dforge-core/metadata": "^0.0.30",
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^26.2.0",
|
|
36
|
+
"tsup": "^8.5.1",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"vitest": "^4.1.10"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"sync-schemas": "node scripts/vendor-schemas.cjs",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"install-skills": "node scripts/install-skills.mjs"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -565,10 +565,12 @@ A **domain** is a reusable, named semantic type: a base datatype, a control, siz
|
|
|
565
565
|
- Each entry in `rights` maps entity code → rights string
|
|
566
566
|
- Rights characters: `S` (Select), `I` (Insert), `U` (Update), `D` (Delete), `C` (Clone) for entities, and `E` (Execute) for actions, reports, and folder access (e.g. `"action:create_quote": "E"`). The roles JSON schema enforces `^[SIUDCE]*$`.
|
|
567
567
|
- Every entity should appear in every role (even if only `"S"` for read-only)
|
|
568
|
+
- A role that inserts or updates an entity needs `S` on the target of each editable lookup on it — including another module's entity, keyed `module.entity` (`"parties.party": "S"`). Without it the lookup is hidden from the create and edit forms, and a required one makes the record unsaveable. `module validate` and `module pack` warn about it
|
|
568
569
|
|
|
569
570
|
**Common Mistakes:**
|
|
570
571
|
- ❌ `"entityRights": { ... }` → ✅ Use `"rights": { ... }`
|
|
571
572
|
- ❌ Omitting entities from a role → Entity will be inaccessible to that role
|
|
573
|
+
- ❌ Granting only your own entities when a lookup points at a dependency's → the lookup vanishes from the form
|
|
572
574
|
|
|
573
575
|
### 6. Seed Data (`seed-data/*.json`)
|
|
574
576
|
|
|
@@ -284,7 +284,9 @@ stated trigger; every report's params sit on a dataset.
|
|
|
284
284
|
3. **Reflect the role list back** before computing rights. Get confirmation.
|
|
285
285
|
4. **Show the rights matrix as a table** (rows = entities/actions/reports,
|
|
286
286
|
columns = roles, cells = rights strings), each cell explained by the verb it
|
|
287
|
-
maps to. Get sign-off.
|
|
287
|
+
maps to. Get sign-off. Include a row for every dependency entity a lookup
|
|
288
|
+
points at: a role that inserts or updates the lookup's entity needs
|
|
289
|
+
`"module.entity": "S"` on the target, or the lookup vanishes from the form.
|
|
288
290
|
5. **Column-level security only when the intake asks for it.** Roles grant rights
|
|
289
291
|
per entity, not per field. When a user type must not see specific *columns*
|
|
290
292
|
(salaries, cost prices, personal data), that is an **entity view**: declare
|