@dforge-core/dforge-mcp 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/server.js +158 -10
- package/package.json +2 -2
- package/resources/docs/conventions.md +8 -1
- package/skills/dforge-mcp-author/references/data-views.md +23 -0
- package/skills/dforge-mcp-author/references/formulas.md +2 -0
- package/skills/dforge-mcp-author/references/translations.md +21 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
|
|
|
4
4
|
`0.1.0-rc.N` pre-release tags; the published version is set at publish time via
|
|
5
5
|
the release workflow, so committed `package.json` versions are placeholders.
|
|
6
6
|
|
|
7
|
+
## 0.1.8
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`dforge_module_validate` now flags a grid-style data view over an entity
|
|
11
|
+
with no visible column.** Errors when a column-rendering view (grid, list,
|
|
12
|
+
kanban, calendar, gallery, tree-grid, master-detail, or the default) draws an
|
|
13
|
+
own-module entity that has no *visible scalar column* — a field whose `flags`
|
|
14
|
+
include `V` and whose `columnType` isn't a set (`S`). Without one the view
|
|
15
|
+
renders the runtime empty state *"No visible columns configured for this
|
|
16
|
+
entity."* This mirrors the server's install-time `DataViewVisibleColumnValidator`,
|
|
17
|
+
so authors catch it pre-flight instead of at pack/install. Column-agnostic view
|
|
18
|
+
types (`diagram`, `matrix`, `library`) are exempt; cross-module entities are
|
|
19
|
+
skipped; the check runs after trait expansion so a trait-contributed `V` field
|
|
20
|
+
counts.
|
|
21
|
+
|
|
22
|
+
### Skill
|
|
23
|
+
- `dforge-mcp-author`: documented the **visible-column requirement** for
|
|
24
|
+
column-rendering data views. `references/data-views.md` gains a "the entity
|
|
25
|
+
needs a visible column" rule (visibility is entity-driven via the `V` flag, not
|
|
26
|
+
the view's `columns` array) plus common-mistake bullets, and
|
|
27
|
+
`references/validation-checklist.md` gains matching checklist/red-flag entries.
|
|
28
|
+
|
|
29
|
+
## 0.1.7
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Bumped `@dforge-core/dforge-cli` to `^0.2.7`. Picks up the corrected module
|
|
33
|
+
scaffolder (`buildTranslations` now emits the nested runtime format with the
|
|
34
|
+
completeness-required `roles` block and opt-in constraint-message localization,
|
|
35
|
+
replacing the non-functional flat shape) and the native CLI's install-time
|
|
36
|
+
untranslated-constraint warning.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **`dforge_module_validate` now flags untranslated check/unique constraint
|
|
40
|
+
messages.** When the manifest declares `supportedLocales`, the validator
|
|
41
|
+
warns (never errors) for every constraint that declares a `message` but has no
|
|
42
|
+
`entities.<e>.constraints.<c>.message` override in a declared non-English
|
|
43
|
+
locale file. This mirrors the server's install-time `UntranslatedConstraint`
|
|
44
|
+
scan, so authors catch the gap pre-flight instead of at install. English
|
|
45
|
+
locales and extension entities (`extends`) are skipped; the locale file is
|
|
46
|
+
matched case-insensitively (`de-de.json` satisfies `de-DE`). Opt-in — modules
|
|
47
|
+
without `supportedLocales` are not scanned.
|
|
48
|
+
|
|
49
|
+
### Skill
|
|
50
|
+
- `dforge-mcp-author`: documented **localizable constraint violation messages**.
|
|
51
|
+
A constraint's `message` in the entity JSON is the base/fallback text; a
|
|
52
|
+
per-locale override under `entities.<e>.constraints.<c>.message` in
|
|
53
|
+
`translations/<locale>.json` localizes it (culture fallback: per-locale →
|
|
54
|
+
base), surfacing identically on the client pre-save validator and the server
|
|
55
|
+
DB-violation path. Localization is opt-in (warned, not completeness-enforced)
|
|
56
|
+
(`translations.md`, `formulas.md`, `validation-checklist.md`,
|
|
57
|
+
`resources/docs/conventions.md`).
|
|
58
|
+
|
|
7
59
|
## 0.1.6
|
|
8
60
|
|
|
9
61
|
### Skill
|
package/dist/server.js
CHANGED
|
@@ -30954,7 +30954,7 @@ var import_node_crypto = require("crypto");
|
|
|
30954
30954
|
var fs2 = __toESM(require("fs"));
|
|
30955
30955
|
var path2 = __toESM(require("path"));
|
|
30956
30956
|
|
|
30957
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
30957
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.7/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
30958
30958
|
function buildManifest(opts, moduleId) {
|
|
30959
30959
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
30960
30960
|
const manifest = {
|
|
@@ -30982,13 +30982,25 @@ function buildManifest(opts, moduleId) {
|
|
|
30982
30982
|
}
|
|
30983
30983
|
function buildEntity(entity) {
|
|
30984
30984
|
const traits2 = entity.traits === "identity+audit" ? ["identity", "audit"] : ["identity"];
|
|
30985
|
-
|
|
30985
|
+
const built = {
|
|
30986
30986
|
description: entity.label,
|
|
30987
30987
|
dbObject: entity.name,
|
|
30988
30988
|
toString: "{id}",
|
|
30989
30989
|
traits: traits2,
|
|
30990
30990
|
fields: {}
|
|
30991
30991
|
};
|
|
30992
|
+
if (entity.constraints?.length) {
|
|
30993
|
+
built.constraints = Object.fromEntries(
|
|
30994
|
+
entity.constraints.map((c) => {
|
|
30995
|
+
const def = { type: c.type ?? "check" };
|
|
30996
|
+
if (c.expression !== void 0) def.expression = c.expression;
|
|
30997
|
+
if (c.fields !== void 0) def.fields = c.fields;
|
|
30998
|
+
def.message = c.message;
|
|
30999
|
+
return [c.name, def];
|
|
31000
|
+
})
|
|
31001
|
+
);
|
|
31002
|
+
}
|
|
31003
|
+
return built;
|
|
30992
31004
|
}
|
|
30993
31005
|
function buildDataViews(entities) {
|
|
30994
31006
|
const views = {};
|
|
@@ -31056,15 +31068,50 @@ function buildSettings() {
|
|
|
31056
31068
|
return {};
|
|
31057
31069
|
}
|
|
31058
31070
|
function buildTranslations(opts) {
|
|
31059
|
-
const
|
|
31060
|
-
t[`module.${opts.code}.label`] = opts.displayName;
|
|
31061
|
-
if (opts.description) t[`module.${opts.code}.description`] = opts.description;
|
|
31071
|
+
const entities = {};
|
|
31062
31072
|
for (const e of opts.entities) {
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31073
|
+
const fields = {
|
|
31074
|
+
// identity trait → primary key column
|
|
31075
|
+
[`${e.name}_id`]: { label: `${e.label} ID` }
|
|
31076
|
+
};
|
|
31077
|
+
if (e.traits === "identity+audit") {
|
|
31078
|
+
fields.created_date = { label: "Created Date" };
|
|
31079
|
+
fields.last_updated = { label: "Last Updated" };
|
|
31080
|
+
}
|
|
31081
|
+
const entityEntry = { label: e.label };
|
|
31082
|
+
if (opts.description && opts.entities.length === 1) {
|
|
31083
|
+
entityEntry.desc = opts.description;
|
|
31084
|
+
}
|
|
31085
|
+
entityEntry.fields = fields;
|
|
31086
|
+
if (e.constraints?.length) {
|
|
31087
|
+
entityEntry.constraints = Object.fromEntries(
|
|
31088
|
+
e.constraints.map((c) => [c.name, { message: c.message }])
|
|
31089
|
+
);
|
|
31090
|
+
}
|
|
31091
|
+
entities[e.name] = entityEntry;
|
|
31066
31092
|
}
|
|
31067
|
-
return
|
|
31093
|
+
return {
|
|
31094
|
+
entities,
|
|
31095
|
+
folders: {
|
|
31096
|
+
// folders.json root folder key is the module code (see buildFolders).
|
|
31097
|
+
[opts.code]: { label: opts.displayName }
|
|
31098
|
+
},
|
|
31099
|
+
roles: {
|
|
31100
|
+
// Role labels are completeness-enforced in every locale incl. en-US.
|
|
31101
|
+
[`${opts.code}.admin`]: { label: `${opts.displayName} Administrator` }
|
|
31102
|
+
},
|
|
31103
|
+
views: Object.fromEntries(
|
|
31104
|
+
opts.entities.map((e) => [e.name, { label: e.label }])
|
|
31105
|
+
),
|
|
31106
|
+
menus: {
|
|
31107
|
+
[`${opts.code}_menu`]: {
|
|
31108
|
+
label: opts.displayName,
|
|
31109
|
+
items: Object.fromEntries(
|
|
31110
|
+
opts.entities.map((e) => [e.name, { label: e.label }])
|
|
31111
|
+
)
|
|
31112
|
+
}
|
|
31113
|
+
}
|
|
31114
|
+
};
|
|
31068
31115
|
}
|
|
31069
31116
|
function buildSeedData() {
|
|
31070
31117
|
return [];
|
|
@@ -33855,6 +33902,38 @@ var SYSTEM_ENTITY_PK = {
|
|
|
33855
33902
|
menu_item: "menu_item_id",
|
|
33856
33903
|
resource: "resource_id"
|
|
33857
33904
|
};
|
|
33905
|
+
var COLUMN_AGNOSTIC_VIEW_TYPES = /* @__PURE__ */ new Set(["diagram", "matrix", "library"]);
|
|
33906
|
+
function hasVisibleScalarColumn(fields) {
|
|
33907
|
+
for (const f of Object.values(fields)) {
|
|
33908
|
+
if (!f || typeof f !== "object") continue;
|
|
33909
|
+
const flags = typeof f.flags === "string" ? f.flags : "";
|
|
33910
|
+
if (flags.includes("V") && f.columnType !== "S") return true;
|
|
33911
|
+
}
|
|
33912
|
+
return false;
|
|
33913
|
+
}
|
|
33914
|
+
function resolveTranslationFile(translationsDir, locale) {
|
|
33915
|
+
const exact = path10.join(translationsDir, `${locale}.json`);
|
|
33916
|
+
if (fs8.existsSync(exact)) return exact;
|
|
33917
|
+
if (!fs8.existsSync(translationsDir)) return void 0;
|
|
33918
|
+
const want = `${locale}.json`.toLowerCase();
|
|
33919
|
+
for (const f of fs8.readdirSync(translationsDir)) {
|
|
33920
|
+
if (f.toLowerCase() === want) return path10.join(translationsDir, f);
|
|
33921
|
+
}
|
|
33922
|
+
return void 0;
|
|
33923
|
+
}
|
|
33924
|
+
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
33925
|
+
if (!root || typeof root !== "object") return false;
|
|
33926
|
+
const entities = root.entities;
|
|
33927
|
+
if (!entities || typeof entities !== "object") return false;
|
|
33928
|
+
const entity = entities[entityCd];
|
|
33929
|
+
if (!entity || typeof entity !== "object") return false;
|
|
33930
|
+
const constraints = entity.constraints;
|
|
33931
|
+
if (!constraints || typeof constraints !== "object") return false;
|
|
33932
|
+
const ck = constraints[constraintCd];
|
|
33933
|
+
if (!ck || typeof ck !== "object") return false;
|
|
33934
|
+
const msg = ck.message;
|
|
33935
|
+
return typeof msg === "string" && msg.trim() !== "";
|
|
33936
|
+
}
|
|
33858
33937
|
function moduleValidate(args) {
|
|
33859
33938
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
33860
33939
|
const issues = [];
|
|
@@ -33863,6 +33942,7 @@ function moduleValidate(args) {
|
|
|
33863
33942
|
const entityMap = manifest.entities ?? {};
|
|
33864
33943
|
const entities = {};
|
|
33865
33944
|
const columnsOf = {};
|
|
33945
|
+
const fieldDefsOf = {};
|
|
33866
33946
|
for (const [name, relPath] of Object.entries(entityMap)) {
|
|
33867
33947
|
if (name.includes(".")) continue;
|
|
33868
33948
|
const abs = path10.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
@@ -33880,12 +33960,15 @@ function moduleValidate(args) {
|
|
|
33880
33960
|
entities[name] = e;
|
|
33881
33961
|
const fields = e.fields ?? {};
|
|
33882
33962
|
const cols = new Set(Object.keys(fields));
|
|
33963
|
+
let traitFields = {};
|
|
33883
33964
|
const traits2 = e.traits ?? [];
|
|
33884
33965
|
try {
|
|
33885
|
-
|
|
33966
|
+
traitFields = expandTraits(traits2, name);
|
|
33967
|
+
for (const c of Object.keys(traitFields)) cols.add(c);
|
|
33886
33968
|
} catch {
|
|
33887
33969
|
}
|
|
33888
33970
|
columnsOf[name] = cols;
|
|
33971
|
+
fieldDefsOf[name] = { ...traitFields, ...fields };
|
|
33889
33972
|
}
|
|
33890
33973
|
const deps = new Set(Object.keys(manifest.dependencies ?? {}));
|
|
33891
33974
|
const isKnownEntity = (code2) => {
|
|
@@ -33954,6 +34037,27 @@ function moduleValidate(args) {
|
|
|
33954
34037
|
}
|
|
33955
34038
|
}
|
|
33956
34039
|
}
|
|
34040
|
+
const vcSeen = /* @__PURE__ */ new Set();
|
|
34041
|
+
for (const [vcode, v] of Object.entries(views)) {
|
|
34042
|
+
const sources = v.dataSources ?? [];
|
|
34043
|
+
if (sources.length === 0) continue;
|
|
34044
|
+
const viewType = v.viewType ?? "grid";
|
|
34045
|
+
if (COLUMN_AGNOSTIC_VIEW_TYPES.has(viewType)) continue;
|
|
34046
|
+
for (const s of sources) {
|
|
34047
|
+
const ent = s.entityCode;
|
|
34048
|
+
if (!ent) continue;
|
|
34049
|
+
const defs = fieldDefsOf[ent];
|
|
34050
|
+
if (!defs) continue;
|
|
34051
|
+
if (hasVisibleScalarColumn(defs)) continue;
|
|
34052
|
+
const key = `${vcode}\0${ent}`;
|
|
34053
|
+
if (vcSeen.has(key)) continue;
|
|
34054
|
+
vcSeen.add(key);
|
|
34055
|
+
err(
|
|
34056
|
+
`data_views \u2192 ${vcode}`,
|
|
34057
|
+
`view (${viewType}) renders entity '${ent}', which has no visible column \u2014 mark at least one of its fields visible with the 'V' flag (set columns / columnType 'S' don't count for a grid)`
|
|
34058
|
+
);
|
|
34059
|
+
}
|
|
34060
|
+
}
|
|
33957
34061
|
const menus = readJsonOrDefault(paths.menus, {});
|
|
33958
34062
|
const walk = (node, where) => {
|
|
33959
34063
|
if (!node || typeof node !== "object") return;
|
|
@@ -33992,6 +34096,50 @@ function moduleValidate(args) {
|
|
|
33992
34096
|
}
|
|
33993
34097
|
} catch {
|
|
33994
34098
|
}
|
|
34099
|
+
const supportedLocales = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
|
|
34100
|
+
if (supportedLocales.length > 0) {
|
|
34101
|
+
const declared = [];
|
|
34102
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
34103
|
+
if (typeof e.extends === "string" && e.extends) continue;
|
|
34104
|
+
const constraints = e.constraints;
|
|
34105
|
+
if (!constraints || typeof constraints !== "object") continue;
|
|
34106
|
+
for (const [cname, c] of Object.entries(constraints)) {
|
|
34107
|
+
if (!c || typeof c !== "object") continue;
|
|
34108
|
+
const msg = c.message;
|
|
34109
|
+
if (typeof msg === "string" && msg.trim() !== "") {
|
|
34110
|
+
declared.push({ entity: name, constraint: cname, message: msg });
|
|
34111
|
+
}
|
|
34112
|
+
}
|
|
34113
|
+
}
|
|
34114
|
+
if (declared.length > 0) {
|
|
34115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34116
|
+
for (const raw of supportedLocales) {
|
|
34117
|
+
const locale = raw.trim();
|
|
34118
|
+
if (!locale) continue;
|
|
34119
|
+
const lc = locale.toLowerCase();
|
|
34120
|
+
if (lc === "en" || lc.startsWith("en-")) continue;
|
|
34121
|
+
if (seen.has(lc)) continue;
|
|
34122
|
+
seen.add(lc);
|
|
34123
|
+
let tx = null;
|
|
34124
|
+
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
34125
|
+
if (abs) {
|
|
34126
|
+
try {
|
|
34127
|
+
tx = JSON.parse(fs8.readFileSync(abs, "utf8"));
|
|
34128
|
+
} catch {
|
|
34129
|
+
tx = null;
|
|
34130
|
+
}
|
|
34131
|
+
}
|
|
34132
|
+
for (const d of declared) {
|
|
34133
|
+
if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
|
|
34134
|
+
warn(
|
|
34135
|
+
`translations/${locale}.json`,
|
|
34136
|
+
`constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
|
|
34137
|
+
);
|
|
34138
|
+
}
|
|
34139
|
+
}
|
|
34140
|
+
}
|
|
34141
|
+
}
|
|
34142
|
+
}
|
|
33995
34143
|
const errors = issues.filter((i) => i.level === "error");
|
|
33996
34144
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
33997
34145
|
const clean = errors.length === 0 && warnings.length === 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes scaffold/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "vitest run"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
34
|
+
"@dforge-core/dforge-cli": "^0.2.7",
|
|
35
35
|
"@dforge-core/metadata": "^0.0.5",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"zod": "^4.4.3"
|
|
@@ -581,6 +581,11 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
581
581
|
"department_id": { "label": "Department ID" },
|
|
582
582
|
"department_name": { "label": "Department Name" }
|
|
583
583
|
}
|
|
584
|
+
},
|
|
585
|
+
"employee": {
|
|
586
|
+
"constraints": {
|
|
587
|
+
"chk_salary_positive": { "message": "Salary must be positive" }
|
|
588
|
+
}
|
|
584
589
|
}
|
|
585
590
|
},
|
|
586
591
|
"folders": {
|
|
@@ -599,7 +604,9 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
599
604
|
}
|
|
600
605
|
```
|
|
601
606
|
|
|
602
|
-
**Translatable sections (consumed by the installer):** `entities` (+ nested `fields`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
|
|
607
|
+
**Translatable sections (consumed by the installer):** `entities` (+ nested `fields` and `constraints`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
|
|
608
|
+
|
|
609
|
+
**Constraint violation messages are localizable (opt-in).** The `message` on a check/unique constraint in the entity JSON is the base (fallback) text. To localize it, add a per-locale override under `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The server resolves it with culture fallback (per-locale → base) and it surfaces identically on the client pre-save validator and the server DB-violation path. Unlike labels, constraint overrides are **not** completeness-enforced: a missing override for a declared `supportedLocales` entry emits a **non-fatal warning** (from `dforge_module_validate` pre-flight and at install) and the base message is used as the fallback.
|
|
603
610
|
|
|
604
611
|
**Silently ignored at runtime:** `roles` and `print_templates` — the resource rows have no `res_id`, so translations are reserved for future use. `settings` is validated for completeness (when listed in `supportedLocales`) but the registrar does not display the translated labels, so don't expect localized output. Display names for these come from the source manifest (`description` for roles, `label` for print templates and settings).
|
|
605
612
|
|
|
@@ -58,6 +58,28 @@ Lives in: `ui/data_views.json`
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
## Critical rule — the entity needs a visible column
|
|
62
|
+
|
|
63
|
+
A grid-style view can only render an entity that has at least one **visible scalar column** — a field whose `flags` include `V` and whose `columnType` is **not** a set (`S`). Column visibility is entity-driven (the field's flags), **not** view-driven: the `columns` array inside `dataSources[]` is layout-only (reorder/pin/width among already-visible fields) — it can't make a non-`V` field appear.
|
|
64
|
+
|
|
65
|
+
If none of an entity's fields are visible (or only its set/child-collection columns are), the view renders the runtime empty state *"No visible columns configured for this entity."* — a broken screen the user only hits when they open the menu item. `dforge_module_validate` and `dforge_module_pack` now **reject** this before install.
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
// WRONG — grid over an entity whose fields are all hidden (no "V")
|
|
69
|
+
"fields": {
|
|
70
|
+
"code": { "fieldTypeCd": "text", "flags": "EM" }, // E=editable, M=… but no V
|
|
71
|
+
"lines": { "columnType": "S", "flags": "VEM" } // visible, but a SET — doesn't count for a grid
|
|
72
|
+
}
|
|
73
|
+
// RIGHT — at least one visible scalar field
|
|
74
|
+
"fields": {
|
|
75
|
+
"code": { "fieldTypeCd": "text", "flags": "VEM" } // V present, not a set → satisfies the grid
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This applies to every column-rendering view type (grid, list, kanban, calendar, gallery, tree-grid, master-detail, and the default). The column-agnostic types — `diagram`, `matrix`, `library` — draw from `viewConfig`/relationships instead, so they're **exempt** from this rule.
|
|
80
|
+
|
|
81
|
+
See [flags.md](flags.md) for the flag string, [column-types.md](column-types.md) for `columnType` (`R`/`S`/`F`).
|
|
82
|
+
|
|
61
83
|
## View types
|
|
62
84
|
|
|
63
85
|
| `viewType` | Description | `viewConfig` | When to use |
|
|
@@ -243,3 +265,4 @@ Most views only have one source. Order is view-level and applies to the primary
|
|
|
243
265
|
- Object-array order like `[{ column_cd: "...", direction: "asc" }]` — **wrong shape for data views**. Use `string[]` with leading `-` for descending.
|
|
244
266
|
- `viewType` is **optional** — omitted or unknown values fall back to `grid` at runtime. Set it explicitly for any non-grid view (`kanban`, `calendar`, `matrix`, …); a `matrix` view also **requires** a `viewConfig` with `rowAxis`/`colAxis`/`cell`.
|
|
245
267
|
- Inventing view types like `"spreadsheet"` or `"table"` — **wrong**. Use `grid`.
|
|
268
|
+
- A grid-style view over an entity with **no visible scalar column** (no field flagged `V`, or only set columns) — **rejected** at validate/pack. Mark at least one non-set field visible. See the [critical rule above](#critical-rule--the-entity-needs-a-visible-column). `diagram`/`matrix`/`library` are exempt.
|
|
@@ -134,6 +134,8 @@ Check constraints use a subset of the formula grammar. The server parses them to
|
|
|
134
134
|
[email] LIKE '%@%'
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
The constraint's `message` (the violation text shown to users) is **localizable** — add a per-locale override at `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The base `message` in the entity JSON is the fallback. See `translations.md` → "Constraint violation messages ARE translatable".
|
|
138
|
+
|
|
137
139
|
## Examples — full formula columns
|
|
138
140
|
|
|
139
141
|
### Full name
|
|
@@ -84,6 +84,26 @@ Translation files mirror the module's content structure. Each translatable objec
|
|
|
84
84
|
| `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
|
|
85
85
|
| `actions` | Action labels, descriptions, and param labels | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label } } }` |
|
|
86
86
|
| `reports` | Report dataset captions and param labels | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label } } }` |
|
|
87
|
+
| `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
|
|
88
|
+
|
|
89
|
+
### Constraint violation messages ARE translatable (opt-in)
|
|
90
|
+
|
|
91
|
+
A check/unique constraint declares a user-facing `message` in the entity JSON (`constraints.<name>.message`). That message is the **base/fallback text**. To localize it, add a per-locale override under `entities.<entityCd>.constraints.<constraintName>.message`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
"entities": {
|
|
95
|
+
"employee": {
|
|
96
|
+
"constraints": {
|
|
97
|
+
"chk_salary_positive": { "message": "Gehalt muss positiv sein" },
|
|
98
|
+
"UQ_employee_code": { "message": "Mitarbeiter-Code muss eindeutig sein" }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- The server resolves the message with culture fallback — **per-locale override → base message from the entity JSON**. `EntityMetadataLoader` reads `COALESCE(culture_res.label, resource.label, entity_constrains.description)`, so the same override surfaces on the client pre-save `CheckConstraints` validator and the server DB-violation (23514) path.
|
|
105
|
+
- **Opt-in, never mandatory** (unlike roles/labels, which completeness *enforces*). If the manifest lists `supportedLocales` and a constraint `message` has no override for one of those (non-English) locales, install emits a **non-fatal warning** naming the missing `entities.<e>.constraints.<c>.message` keys (printed by the CLI, returned in the marketplace install response). Install still commits with the base message as the fallback. `dforge_module_validate` surfaces the same gap pre-flight as a warning.
|
|
106
|
+
- Only constraints that declare a `message` are scanned; English (`en`/`en-*`) is the base and is never warned; extension entities (`"extends": "..."`) are skipped — their constraint translations belong with the foreign module's files.
|
|
87
107
|
|
|
88
108
|
### Roles ARE translated — and completeness is enforced
|
|
89
109
|
|
|
@@ -173,6 +193,7 @@ Every listed locale MUST have a matching `translations/<locale>.json` with a lab
|
|
|
173
193
|
6. **Missing keys fall back** to the `en-US` value, then to the raw code name. So it's safe to ship partial translations — untranslated items show in English rather than breaking.
|
|
174
194
|
7. **Don't translate column codes** — only the `label` values. Codes stay as-is in all languages.
|
|
175
195
|
8. **File naming**: `translations/<locale>.json` (e.g. `de-DE.json`) — each non-English file must match a locale listed in `supportedLocales`.
|
|
196
|
+
9. **Constraint messages are opt-in** — localize them under `entities.<e>.constraints.<c>.message` when you want a per-locale violation message; otherwise the base `message` from the entity JSON is used. Missing overrides warn (non-fatal), they don't fail install. See "Constraint violation messages ARE translatable" above.
|
|
176
197
|
|
|
177
198
|
## When to create translations
|
|
178
199
|
|
|
@@ -72,6 +72,7 @@ For each entity:
|
|
|
72
72
|
- [ ] Each source has `entityCode` and `columns`
|
|
73
73
|
- [ ] `entityCode` points to an entity that exists
|
|
74
74
|
- [ ] Column codes in `columns` all exist on the entity
|
|
75
|
+
- [ ] The rendered entity has **at least one visible scalar column** (a field with `V` in `flags`, `columnType` not `S`) — a grid/list/kanban/etc. view over an entity whose fields are all hidden (or only sets) is **rejected** at validate/pack and would render "No visible columns configured for this entity." `diagram`/`matrix`/`library` are exempt
|
|
75
76
|
- [ ] If set, `viewType` is from the supported list (`grid`, `list`, `kanban`, `calendar`, `gallery`, `tree-grid`, `diagram`, `master-detail`, `library`, `matrix`)
|
|
76
77
|
- [ ] A `matrix` view has a `viewConfig` with `rowAxis`, `colAxis`, and `cell` (cell `entity` matches the primary `dataSources` entity; `rowKey`/`colKey` are real cell columns)
|
|
77
78
|
- [ ] Sort uses the view-def-root `order` key — a `string[]` like `["-created_date", "name"]` (leading `-` = descending), NOT `sort` / `[{column_cd, direction}]` (that object shape belongs to queries & reports, not data views)
|
|
@@ -133,6 +134,7 @@ For each action:
|
|
|
133
134
|
- [ ] Has `menus` section matching the `ui/menus.json` structure
|
|
134
135
|
- [ ] Has `actions` section with labels for every action
|
|
135
136
|
- [ ] Has `roles` section with a `label` for **every** role in `security/roles.json` (completeness-enforced — a missing role label fails install; keys are the module-qualified role codes, e.g. `crm.admin`)
|
|
137
|
+
- [ ] (Optional) Constraint messages localized under `entities.<e>.constraints.<c>.message` — opt-in; a missing override for a declared `supportedLocales` entry warns (surfaced by `dforge_module_validate` and at install) but does not fail install
|
|
136
138
|
- [ ] Has `settings` section with labels for every setting (if settings exist)
|
|
137
139
|
- [ ] Has `folders` section with label for the root folder
|
|
138
140
|
- [ ] Additional language files (if any) have the same structure as `en-US.json`
|
|
@@ -171,6 +173,7 @@ If you see any of these, stop and investigate:
|
|
|
171
173
|
- Menus with `children: [...]` (array, not dict)
|
|
172
174
|
- Roles with `entityRights` (wrong key)
|
|
173
175
|
- Data views with root-level `entityCode` (should be inside `dataSources`)
|
|
176
|
+
- A grid/list/kanban/etc. view over an entity with no field flagged `V` (or only set columns visible) — rejected at validate/pack
|
|
174
177
|
- Formula columns without `baseDatatypeCd`
|
|
175
178
|
- DSL actions with JavaScript/Python syntax
|
|
176
179
|
- Seed data files without numbered prefixes
|