@dforge-core/metadata 0.0.23 → 0.0.25

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/src/dsl/types.ts CHANGED
@@ -79,6 +79,14 @@ export interface DslContext {
79
79
  action?: {
80
80
  code: string;
81
81
  executionMode?: ActionExecutionMode;
82
+ /**
83
+ * True when a scheduled job invokes this action. A job runs as the
84
+ * system user with no current record, so record context is a hard
85
+ * error there even in a mode that would otherwise allow it. Only a
86
+ * reader that has the module's jobs can know this, so it is the host's
87
+ * to supply; absent, the rule stands down like every other.
88
+ */
89
+ viaJob?: boolean;
82
90
  };
83
91
  /**
84
92
  * Entity behind the record context (`[field]`). `null` or absent when it
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export type {
28
28
  FlagCd,
29
29
  NamedKind,
30
30
  TraitDef,
31
+ TraitsFile,
31
32
  } from "./types";
32
33
 
33
34
  export { AggType, AGG_TYPE_LIST } from "./aggregation";
package/src/traits.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // these as checkboxes and previews the expanded fields, so authors never
7
7
  // hand-write `created_date` / `{entity}_id` etc.
8
8
 
9
- import type { FieldDef, TraitDef } from "./types";
9
+ import type { FieldDef, TraitDef, TraitsFile } from "./types";
10
10
 
11
11
  export const traits: readonly TraitDef[] = [
12
12
  {
@@ -107,21 +107,61 @@ function templateFields(fields: Record<string, FieldDef>, entity: string): Recor
107
107
  * for `entityName`, with `{entity}` / `{Entity}` tokens resolved. Returns an
108
108
  * empty object for unknown or marker traits. Field order follows include-depth
109
109
  * first, then declaration order; later definitions win on key collisions.
110
+ *
111
+ * `localTraits` is a module's own `traits.json`. A module may declare traits of
112
+ * its own, and an entity in it names them exactly like a platform trait — so a
113
+ * reader that knows only this registry sees an entity missing columns that
114
+ * install will give it. Pass the file and those traits resolve; a code declared
115
+ * in both wins locally, mirroring the installer, which overlays the module's
116
+ * traits on the platform ones (`TraitExpanderFactory.ForPackage`).
110
117
  */
111
- export function expandTrait(traitCd: string, entityName: string): Record<string, FieldDef> {
112
- const trait = byCd.get(traitCd);
113
- if (!trait) return {};
118
+ export function expandTrait(
119
+ traitCd: string,
120
+ entityName: string,
121
+ localTraits?: TraitsFile,
122
+ ): Record<string, FieldDef> {
123
+ return expandTraitInner(traitCd, entityName, localTraits, new Set());
124
+ }
125
+
126
+ function expandTraitInner(
127
+ traitCd: string,
128
+ entityName: string,
129
+ localTraits: TraitsFile | undefined,
130
+ path: Set<string>,
131
+ ): Record<string, FieldDef> {
132
+ // `includes` is author-supplied once local traits are in play, so a cycle is
133
+ // reachable from a module file. Unguarded that is an editor hang, not a bad
134
+ // expansion.
135
+ //
136
+ // The guard is the current path, not every trait already visited: in a
137
+ // diamond — two includes that both reach a third — the third has to expand
138
+ // under each of them, or which definition wins a key collision would depend
139
+ // on which branch got there first.
140
+ if (path.has(traitCd)) return {};
141
+ path.add(traitCd);
142
+
143
+ const trait = localTraits?.[traitCd] ?? byCd.get(traitCd);
114
144
  const result: Record<string, FieldDef> = {};
115
- for (const included of trait.includes ?? []) {
116
- Object.assign(result, expandTrait(included, entityName));
145
+ if (trait) {
146
+ for (const included of trait.includes ?? []) {
147
+ Object.assign(result, expandTraitInner(included, entityName, localTraits, path));
148
+ }
149
+ Object.assign(result, templateFields(trait.fields ?? {}, entityName));
117
150
  }
118
- Object.assign(result, templateFields(trait.fields, entityName));
151
+
152
+ path.delete(traitCd);
119
153
  return result;
120
154
  }
121
155
 
122
156
  /** Expand several traits in order into one merged field map. */
123
- export function expandTraits(traitCds: readonly string[], entityName: string): Record<string, FieldDef> {
157
+ export function expandTraits(
158
+ traitCds: readonly string[],
159
+ entityName: string,
160
+ localTraits?: TraitsFile,
161
+ ): Record<string, FieldDef> {
124
162
  const result: Record<string, FieldDef> = {};
125
- for (const cd of traitCds) Object.assign(result, expandTrait(cd, entityName));
163
+ for (const cd of traitCds) {
164
+ Object.assign(result, expandTraitInner(cd, entityName, localTraits, new Set()));
165
+ }
126
166
  return result;
127
167
  }
package/src/types.ts CHANGED
@@ -179,6 +179,21 @@ export interface TraitDef {
179
179
  references?: Record<string, unknown>;
180
180
  }
181
181
 
182
+ /**
183
+ * A module's own `traits.json`, keyed by trait code — the key is the `cd`, so a
184
+ * definition here is otherwise a {@link TraitDef} with everything optional
185
+ * (a marker trait declares no fields at all).
186
+ */
187
+ export interface TraitsFile {
188
+ [traitCd: string]: {
189
+ description?: string;
190
+ includes?: string[];
191
+ fields?: Record<string, FieldDef>;
192
+ references?: Record<string, unknown>;
193
+ constraints?: Record<string, unknown>;
194
+ };
195
+ }
196
+
182
197
  /** Description of a column kind for friendly pickers. */
183
198
  export interface ColumnTypeDef {
184
199
  cd: ColumnTypeCd;