@dforge-core/metadata 0.0.25 → 0.0.27

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 CHANGED
@@ -5,6 +5,35 @@ All notable changes to `@dforge-core/metadata` are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.26] — 2026-09-15
9
+
10
+ ### Added
11
+
12
+ - **`traitFieldConflicts()` and the `TraitFieldConflict` type** report the collisions
13
+ that make the installer refuse an entity: a trait contributing a field the entity
14
+ already has under a different type identity (`columnType ?? dbDatatype ??
15
+ fieldTypeCd`, the identity `TraitExpander.TryAddField` compares). Install throws on
16
+ one, so a module that hits it cannot install — offline readers now name the field,
17
+ both types, and whether the definition being kept is an authored field or another
18
+ trait's. Expansion itself stays total, so an editor can still draw a field list.
19
+
20
+ ### Fixed
21
+
22
+ - **A field code that names an inherited property is a field like any other.** The
23
+ merge read `sink.fields[key]` against a plain object, so a trait defining
24
+ `constructor` found `Object.prototype`'s, contributed no column, and reported a
25
+ conflict against a type identity read off a function. Own-property lookups
26
+ throughout, on a prototype-less merge map — which also fixes the write side, where a
27
+ field named `__proto__` set the prototype instead of a key — and on the
28
+ `localTraits` lookup, which is parsed JSON with the same hazard.
29
+
30
+ - **Two templated keys that collide on one entity are a conflict, not a silent
31
+ overwrite.** A trait defining both `{entity}_id` and `x_id` collapsed them while
32
+ templating, for entity `x`, before the merge ever saw them: the later definition won
33
+ and nothing was reported, while install rejected the entity. Each field is now
34
+ resolved and merged on its own, the way the installer resolves and adds one key at a
35
+ time — the first definition wins, the second is reported.
36
+
8
37
  ## [0.0.25] — 2026-09-15
9
38
 
10
39
  ### Added
package/dist/index.d.ts CHANGED
@@ -442,11 +442,33 @@ declare function getColumnType(cd: string): ColumnTypeDef | undefined;
442
442
  declare const traits: readonly TraitDef[];
443
443
  /** Look up a trait by code. */
444
444
  declare function getTrait(cd: string): TraitDef | undefined;
445
+ /** Where a field already in the merge came from, for conflict wording. */
446
+ type FieldOrigin = "field" | "trait";
447
+ /**
448
+ * A trait contributing a field the entity already has under a DIFFERENT type.
449
+ * The installer throws on this (`Field 'x' conflicts with trait: ...`), so a
450
+ * module that hits one cannot install — offline readers report it instead.
451
+ */
452
+ interface TraitFieldConflict {
453
+ /** Column code, after `{entity}` / `{Entity}` resolution. */
454
+ field: string;
455
+ /** Whether the definition being kept is an authored field or a trait's. */
456
+ existingFrom: FieldOrigin;
457
+ /** Type identity of the definition being kept. */
458
+ existingType?: string;
459
+ /** Type identity of the trait definition being dropped. */
460
+ traitType?: string;
461
+ }
445
462
  /**
446
463
  * Expand a trait (and any traits it includes) into the concrete fields it adds
447
464
  * for `entityName`, with `{entity}` / `{Entity}` tokens resolved. Returns an
448
465
  * empty object for unknown or marker traits. Field order follows include-depth
449
- * first, then declaration order; later definitions win on key collisions.
466
+ * first, then declaration order.
467
+ *
468
+ * The FIRST definition of a key wins, mirroring the installer's
469
+ * `TraitExpander.TryAddField`. A later definition of the same key under a
470
+ * different type is one the installer refuses to expand at all — see
471
+ * {@link traitFieldConflicts}, which reports those without throwing.
450
472
  *
451
473
  * `localTraits` is a module's own `traits.json`. A module may declare traits of
452
474
  * its own, and an entity in it names them exactly like a platform trait — so a
@@ -458,6 +480,24 @@ declare function getTrait(cd: string): TraitDef | undefined;
458
480
  declare function expandTrait(traitCd: string, entityName: string, localTraits?: TraitsFile): Record<string, FieldDef>;
459
481
  /** Expand several traits in order into one merged field map. */
460
482
  declare function expandTraits(traitCds: readonly string[], entityName: string, localTraits?: TraitsFile): Record<string, FieldDef>;
483
+ /**
484
+ * The collisions that make the installer refuse an entity: a trait contributing
485
+ * a field the entity already has under a different type identity.
486
+ *
487
+ * `fields` is the entity's own authored fields. The installer seeds its merge
488
+ * with them and expands traits into it (`TraitExpander.ExpandAll`), so an
489
+ * authored field can conflict with a trait's exactly as two traits can — and
490
+ * either way install fails with `Field 'x' conflicts with trait`. Omit it to
491
+ * check the traits against each other alone.
492
+ *
493
+ * Returns one entry per field, in the order the conflicts are reached. An empty
494
+ * array means the expansion the installer performs is the one
495
+ * {@link expandTraits} returns.
496
+ */
497
+ declare function traitFieldConflicts(traitCds: readonly string[], entityName: string, opts?: {
498
+ localTraits?: TraitsFile;
499
+ fields?: Record<string, FieldDef>;
500
+ }): TraitFieldConflict[];
461
501
 
462
502
  /** Total digits used when building a `numeric(total, scale)` type. */
463
503
  declare const NUMERIC_TOTAL = 18;
@@ -633,8 +673,8 @@ interface AccumulationConfig {
633
673
  sign?: SignConfig;
634
674
  autoCreateBalance?: boolean;
635
675
  allowNegative?: boolean;
636
- /** Document fields locked once posted. */
637
- lockedFields?: string[];
676
+ /** Document fields locked once posted. Required; `["*"]` freezes the whole row. */
677
+ lockedFields: string[];
638
678
  }
639
679
  /** One registry target within an L-column's `registries` array. */
640
680
  interface LedgerRegistryEntry {
@@ -668,6 +708,8 @@ interface EntityDef {
668
708
  viewSql?: string;
669
709
  /** Display pattern using column placeholders, e.g. "{first_name} {last_name}". */
670
710
  toString?: string;
711
+ /** Records of this entity accept comments (composer + thread on the card). */
712
+ comments?: boolean;
671
713
  /** Traits expanded at install time. */
672
714
  traits?: TraitCd[];
673
715
  /** Column definitions keyed by column code. Optional when a trait (e.g. `period`) supplies all columns. */
@@ -716,7 +758,7 @@ declare function getIdentityKeys(entity: EntityDef, entityCode: string): string[
716
758
  declare function getPrimaryKeys(entity: EntityDef, entityCode: string): string[];
717
759
 
718
760
  /** `viewType` values for `ui/data_views.json`. Source: data_views.schema.json. */
719
- type ViewType = "grid" | "list" | "kanban" | "calendar" | "gallery" | "tree-grid" | "diagram" | "master-detail" | "library" | "matrix";
761
+ type ViewType = "grid" | "list" | "kanban" | "calendar" | "gallery" | "tree-grid" | "diagram" | "library" | "matrix";
720
762
  /** Friendly picker list of the view kinds. */
721
763
  declare const dataViewKinds: readonly NamedKind[];
722
764
  /** A column in a data source: shorthand string code or a configured object. */
@@ -736,7 +778,7 @@ interface DataSource {
736
778
  entityCode: string;
737
779
  /** Nesting level (0 = root, 1+ = detail). */
738
780
  level?: number;
739
- /** Human-readable label (used in master-detail tabs). */
781
+ /** Human-readable label for this data source. */
740
782
  label?: string;
741
783
  /** Column configuration. */
742
784
  columns?: ViewColumn[];
@@ -795,7 +837,7 @@ interface DataViewDef {
795
837
  /** Bootstrap icon class (e.g. 'bi-bounding-box'). */
796
838
  icon?: string;
797
839
  description?: string;
798
- /** Data sources (at least one; master-detail needs two). */
840
+ /** Data sources (at least one). Only the first is rendered today. */
799
841
  dataSources: DataSource[];
800
842
  /** Default filter applied to all sources unless one declares its own. */
801
843
  filter?: Filter;
@@ -1024,6 +1066,8 @@ interface ManifestDef {
1024
1066
  dependencies?: Record<string, ModuleDependency>;
1025
1067
  /** Default audit history mode for this module's entities. */
1026
1068
  auditHistory?: "none" | "minimal" | "full";
1069
+ /** Default for `comments` on this module's entities; each entity may override. */
1070
+ comments?: boolean;
1027
1071
  /** Entity code → relative path of its JSON file (dotted keys = extensions). */
1028
1072
  entities?: Record<string, string>;
1029
1073
  /** Module category for display (e.g. 'Integration', 'Finance'). */
@@ -1382,6 +1426,95 @@ interface PrintTemplateDef {
1382
1426
  /** A `ui/print_templates.json` file: template_cd → definition. */
1383
1427
  type PrintTemplatesFile = Record<string, PrintTemplateDef>;
1384
1428
 
1429
+ /** A field in a card section — either a bare column code or pixel sizing with it. */
1430
+ type CardFieldEntry = string | CardFieldDef;
1431
+ interface CardFieldDef {
1432
+ column_cd: string;
1433
+ /** Field width in px. */
1434
+ width?: number;
1435
+ /** Field height in px — for multiline/textarea fields. */
1436
+ height?: number;
1437
+ }
1438
+ /** A group of fields laid out in 1, 2 or 3 columns. */
1439
+ interface ColumnGroupSection {
1440
+ type: "columnGroup";
1441
+ code: string;
1442
+ /** Section heading, in the module's authoring language. The installer gives it an
1443
+ * `entity_column_group` row keyed on this section's `code`, so
1444
+ * `translations/<locale>.json` localizes it under
1445
+ * `entities.<entity>.columnGroups.<code>.label` and the card renders that instead. */
1446
+ label?: string;
1447
+ columns: CardFieldEntry[];
1448
+ /** Field columns across the group (1-3). Defaults to the card's own setting. */
1449
+ cols?: number;
1450
+ }
1451
+ /**
1452
+ * An embedded detail grid for a 1:N set column — the master/detail building
1453
+ * block. The label comes from the set column's own metadata.
1454
+ */
1455
+ interface SetSection {
1456
+ type: "set";
1457
+ code: string;
1458
+ /** Set column code on this entity (columnType 'S'). */
1459
+ setField: string;
1460
+ }
1461
+ /** Reference to a tab item: a columnGroup section's code, or a set column's code. */
1462
+ interface CardTabRef {
1463
+ type: "section" | "set";
1464
+ code: string;
1465
+ }
1466
+ /** A tab bar whose tabs are other sections — several detail grids without one long form. */
1467
+ interface TabGroupSection {
1468
+ type: "tabGroup";
1469
+ code: string;
1470
+ tabs: CardTabRef[];
1471
+ }
1472
+ type CardSection = ColumnGroupSection | SetSection | TabGroupSection;
1473
+ /** Per-set-field renderer choice, keyed by the set column code. */
1474
+ interface CardSetConfig {
1475
+ viewType?: "grid" | "list";
1476
+ /** Opaque per-renderer options, passed verbatim to the set registration. */
1477
+ options?: Record<string, unknown>;
1478
+ }
1479
+ /** The layout body, stored verbatim into `dForge.entity_view_layout.layout`. */
1480
+ interface CardLayout {
1481
+ sections: CardSection[];
1482
+ sets?: Record<string, CardSetConfig>;
1483
+ }
1484
+ /**
1485
+ * A module-shipped card layout (value in the card_layouts map; the key is the
1486
+ * layout name).
1487
+ *
1488
+ * Rows written from here carry `module_id`, so the installer recreates them on
1489
+ * every install and removes them on uninstall. A layout the tenant drew in the
1490
+ * card editor has `module_id` NULL and is never touched.
1491
+ */
1492
+ interface CardLayoutDef {
1493
+ /**
1494
+ * Entity code the layout belongs to. Unqualified means this module owns the
1495
+ * entity; `module.entity` targets another module's (the bridge case).
1496
+ */
1497
+ entity: string;
1498
+ /**
1499
+ * Entity view the layout hangs off. Defaults to `"default"` — the same
1500
+ * fallback the runtime uses for an entity with no folder binding of its own.
1501
+ * Matched against the entity's declared views case-insensitively, and filed
1502
+ * under their spelling.
1503
+ */
1504
+ view?: string;
1505
+ /**
1506
+ * Whether this layout opens by default. Honoured on FIRST install only, and
1507
+ * only when the view has no default yet: once a tenant has chosen their own,
1508
+ * an upgrade does not take it back.
1509
+ */
1510
+ isDefault?: boolean;
1511
+ /** Human-readable note for the package author. Not stored. */
1512
+ description?: string;
1513
+ layout: CardLayout;
1514
+ }
1515
+ /** `ui/card_layouts.json` — layout name → definition. */
1516
+ type CardLayoutsFile = Record<string, CardLayoutDef>;
1517
+
1385
1518
  /** A seed-data file: records to insert into one entity. */
1386
1519
  interface SeedDataFile {
1387
1520
  /** Target entity code. */
@@ -1395,4 +1528,4 @@ declare const flagDefs: readonly {
1395
1528
  label: string;
1396
1529
  }[];
1397
1530
 
1398
- export { AGG_TYPE_LIST, type AccumulationConfig, AggType, type AlignType, type BaseDatatypeCd, type CalendarViewConfig, type ColumnDef, type ColumnTypeCd, type ColumnTypeDef, DEP_COLUMN_TYPES, type DataSource, type DataViewDef, type DataViewsFile, type Dataset, type DepColumn, type DepColumnMode, type DepColumnType, type DepEntity, type DepProvenance, type DepProvenanceKind, type DepsFile, type DeriveOptions, type DiagramViewConfig, type DomainDef, type DomainsFile, type EntityDef, type EntityEvent, type EntityReference, type EntityViewColumnDef, type EntityViewDef, FILTER_GROUP_OPERATORS, FILTER_OPERATORS, type FieldDef, type FieldLink, type FieldOption, type FieldTypeCd, type FieldTypeDef, type Filter, type FilterCondition, type FilterGroup, type FilterGroupOperator, type FilterOperator, type FlagCd, type FolderDef, type FolderEntityBinding, type FoldersFile, type JobDef, type JobsFile, type KanbanViewConfig, type LedgerRegistryEntry, type ListLevelConfig, type ListViewConfig, type ManifestAuthor, type ManifestDef, type MenuDef, type MenuItemDef, type MenuItemType, type MenusFile, type ModuleDependency, type ModuleFeature, NUMERIC_TOTAL, type NamedKind, type NumberSequenceDef, type ParamDef, type ParamDefBase, type PeriodConfig, type PrintMargins, type PrintPageSettings, type PrintTemplateDef, type PrintTemplatesFile, type ReportDef, type ReportEntityAttachment, type ReportLayout, type ReportPanel, type ReportQuery, type ReportsFile, type RoleDef, type RolesFile, type SeedDataFile, type SettingBaseDatatype, type SettingDef, type SettingsFile, type SignConfig, type SortClause, type TraitCd, type TraitDef, type TraitsFile, type TreeGridViewConfig, type TriggerDef, type TriggersFile, type ViewColumn, type ViewType, type VizType, type WebhookPayload, type WebhookSubscription, type WebhooksFile, baseDatatypes, baseToDbDatatype, chartTypes, columnTypes, dataViewKinds, defaultParams, deriveBaseDatatype, deriveDbDatatype, expandTrait, expandTraits, fieldTypeCds, fieldTypes, fieldTypesByColumnType, flagDefs, getColumnType, getFieldType, getIdentityKeys, getLinks, getPrimaryKeys, getTrait, isFieldTypeCd, traits, vizTypes };
1531
+ export { AGG_TYPE_LIST, type AccumulationConfig, AggType, type AlignType, type BaseDatatypeCd, type CalendarViewConfig, type CardFieldDef, type CardFieldEntry, type CardLayout, type CardLayoutDef, type CardLayoutsFile, type CardSection, type CardSetConfig, type CardTabRef, type ColumnDef, type ColumnGroupSection, type ColumnTypeCd, type ColumnTypeDef, DEP_COLUMN_TYPES, type DataSource, type DataViewDef, type DataViewsFile, type Dataset, type DepColumn, type DepColumnMode, type DepColumnType, type DepEntity, type DepProvenance, type DepProvenanceKind, type DepsFile, type DeriveOptions, type DiagramViewConfig, type DomainDef, type DomainsFile, type EntityDef, type EntityEvent, type EntityReference, type EntityViewColumnDef, type EntityViewDef, FILTER_GROUP_OPERATORS, FILTER_OPERATORS, type FieldDef, type FieldLink, type FieldOption, type FieldTypeCd, type FieldTypeDef, type Filter, type FilterCondition, type FilterGroup, type FilterGroupOperator, type FilterOperator, type FlagCd, type FolderDef, type FolderEntityBinding, type FoldersFile, type JobDef, type JobsFile, type KanbanViewConfig, type LedgerRegistryEntry, type ListLevelConfig, type ListViewConfig, type ManifestAuthor, type ManifestDef, type MenuDef, type MenuItemDef, type MenuItemType, type MenusFile, type ModuleDependency, type ModuleFeature, NUMERIC_TOTAL, type NamedKind, type NumberSequenceDef, type ParamDef, type ParamDefBase, type PeriodConfig, type PrintMargins, type PrintPageSettings, type PrintTemplateDef, type PrintTemplatesFile, type ReportDef, type ReportEntityAttachment, type ReportLayout, type ReportPanel, type ReportQuery, type ReportsFile, type RoleDef, type RolesFile, type SeedDataFile, type SetSection, type SettingBaseDatatype, type SettingDef, type SettingsFile, type SignConfig, type SortClause, type TabGroupSection, type TraitCd, type TraitDef, type TraitFieldConflict, type TraitsFile, type TreeGridViewConfig, type TriggerDef, type TriggersFile, type ViewColumn, type ViewType, type VizType, type WebhookPayload, type WebhookSubscription, type WebhooksFile, baseDatatypes, baseToDbDatatype, chartTypes, columnTypes, dataViewKinds, defaultParams, deriveBaseDatatype, deriveDbDatatype, expandTrait, expandTraits, fieldTypeCds, fieldTypes, fieldTypesByColumnType, flagDefs, getColumnType, getFieldType, getIdentityKeys, getLinks, getPrimaryKeys, getTrait, isFieldTypeCd, traitFieldConflicts, traits, vizTypes };
package/dist/index.js CHANGED
@@ -182,39 +182,73 @@ function applyTemplate(value, entity) {
182
182
  const local = entity.split(".").pop() ?? entity;
183
183
  return value.replace(/\{entity\}/g, local).replace(/\{Entity\}/g, titleCaseEntity(entity));
184
184
  }
185
- function templateFields(fields, entity) {
186
- const out = {};
187
- for (const [name, def] of Object.entries(fields)) {
188
- const key = applyTemplate(name, entity);
189
- const next = { ...def };
190
- if (typeof next.description === "string") next.description = applyTemplate(next.description, entity);
191
- out[key] = next;
185
+ function templateField(def, entity) {
186
+ const next = { ...def };
187
+ if (typeof next.description === "string") next.description = applyTemplate(next.description, entity);
188
+ return next;
189
+ }
190
+ function emptyFields() {
191
+ return /* @__PURE__ */ Object.create(null);
192
+ }
193
+ function typeKeyOf(def) {
194
+ return def.columnType ?? def.dbDatatype ?? def.fieldTypeCd;
195
+ }
196
+ function addTraitField(sink, key, def) {
197
+ const existing = sink.fields[key];
198
+ if (!Object.hasOwn(sink.fields, key)) {
199
+ sink.fields[key] = def;
200
+ sink.origin.set(key, "trait");
201
+ return;
202
+ }
203
+ if (sink.conflicts && !sink.conflicts.some((c) => c.field === key)) {
204
+ const existingType = typeKeyOf(existing);
205
+ const traitType = typeKeyOf(def);
206
+ if (existingType !== traitType) {
207
+ sink.conflicts.push({
208
+ field: key,
209
+ existingFrom: sink.origin.get(key) ?? "trait",
210
+ existingType,
211
+ traitType
212
+ });
213
+ }
192
214
  }
193
- return out;
194
215
  }
195
216
  function expandTrait(traitCd, entityName, localTraits) {
196
- return expandTraitInner(traitCd, entityName, localTraits, /* @__PURE__ */ new Set());
217
+ const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map() };
218
+ expandTraitInner(traitCd, entityName, localTraits, /* @__PURE__ */ new Set(), sink);
219
+ return sink.fields;
197
220
  }
198
- function expandTraitInner(traitCd, entityName, localTraits, path) {
199
- if (path.has(traitCd)) return {};
221
+ function expandTraitInner(traitCd, entityName, localTraits, path, sink) {
222
+ if (path.has(traitCd)) return;
200
223
  path.add(traitCd);
201
- const trait = localTraits?.[traitCd] ?? byCd3.get(traitCd);
202
- const result = {};
224
+ const trait = localTraits && Object.hasOwn(localTraits, traitCd) ? localTraits[traitCd] : byCd3.get(traitCd);
203
225
  if (trait) {
204
226
  for (const included of trait.includes ?? []) {
205
- Object.assign(result, expandTraitInner(included, entityName, localTraits, path));
227
+ expandTraitInner(included, entityName, localTraits, path, sink);
228
+ }
229
+ for (const [name, def] of Object.entries(trait.fields ?? {})) {
230
+ addTraitField(sink, applyTemplate(name, entityName), templateField(def, entityName));
206
231
  }
207
- Object.assign(result, templateFields(trait.fields ?? {}, entityName));
208
232
  }
209
233
  path.delete(traitCd);
210
- return result;
211
234
  }
212
235
  function expandTraits(traitCds, entityName, localTraits) {
213
- const result = {};
236
+ const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map() };
214
237
  for (const cd of traitCds) {
215
- Object.assign(result, expandTraitInner(cd, entityName, localTraits, /* @__PURE__ */ new Set()));
238
+ expandTraitInner(cd, entityName, localTraits, /* @__PURE__ */ new Set(), sink);
216
239
  }
217
- return result;
240
+ return sink.fields;
241
+ }
242
+ function traitFieldConflicts(traitCds, entityName, opts = {}) {
243
+ const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map(), conflicts: [] };
244
+ for (const [key, def] of Object.entries(opts.fields ?? {})) {
245
+ sink.fields[key] = def;
246
+ sink.origin.set(key, "field");
247
+ }
248
+ for (const cd of traitCds) {
249
+ expandTraitInner(cd, entityName, opts.localTraits, /* @__PURE__ */ new Set(), sink);
250
+ }
251
+ return sink.conflicts;
218
252
  }
219
253
 
220
254
  // src/derive.ts
@@ -332,7 +366,6 @@ var dataViewKinds = [
332
366
  { cd: "gallery", name: "Gallery" },
333
367
  { cd: "tree-grid", name: "Tree Grid" },
334
368
  { cd: "diagram", name: "Diagram" },
335
- { cd: "master-detail", name: "Master / Detail" },
336
369
  { cd: "library", name: "Library" },
337
370
  { cd: "matrix", name: "Matrix" }
338
371
  ];
@@ -422,6 +455,7 @@ export {
422
455
  getPrimaryKeys,
423
456
  getTrait,
424
457
  isFieldTypeCd,
458
+ traitFieldConflicts,
425
459
  traits,
426
460
  vizTypes
427
461
  };