@xrmforge/devkit 0.5.4 → 0.5.6

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.
@@ -18,8 +18,14 @@ Run `xrmforge generate` to create:
18
18
  - `generated/entities/{entity}.ts` - Entity interface
19
19
  - `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
20
20
  - `generated/entity-names.ts` - EntityNames const enum
21
+ - `generated/form-mapping.json` - Entity to form interface mapping (read after generate!)
21
22
  - `generated/index.ts` - Barrel file with `export * from` re-exports
22
23
 
24
+ **After generate:** Read `generated/form-mapping.json` for the mapping of entity logical
25
+ names to form interface names. Do NOT guess interface names from entity names.
26
+ Fields enum member names are based on the **primary language label** (often German),
27
+ not the logical field name. Always read the generated files to get correct names.
28
+
23
29
  ## Rules: MANDATORY (every violation is a bug)
24
30
 
25
31
  1. **Fields Enum** for ALL getAttribute/getControl calls. Never raw strings.
@@ -42,11 +48,12 @@ Run `xrmforge generate` to create:
42
48
  const form = ctx.getFormContext() as AccountMainForm;
43
49
  ```
44
50
 
45
- 4. **EntityNames Enum** in ALL Xrm.WebApi calls:
51
+ 4. **EntityNames Enum** in ALL Xrm.WebApi calls — no exceptions, even for system entities:
46
52
  ```typescript
47
53
  import { EntityNames } from '../generated/entity-names.js';
48
54
  Xrm.WebApi.retrieveRecord(EntityNames.Account, id)
49
55
  ```
56
+ If an entity is not in EntityNames, extend the generation (`--entities` flag) to include it.
50
57
 
51
58
  5. **Lookup helpers** from @xrmforge/helpers for ALL lookup value access:
52
59
  ```typescript
@@ -57,11 +64,28 @@ Run `xrmforge generate` to create:
57
64
  // Web API response lookups (_fieldname_value + OData annotations):
58
65
  const parent = parseLookup(apiResponse, 'parentaccountid');
59
66
  ```
67
+ **NEVER** access lookup values directly:
68
+ ```typescript
69
+ // BUG - never do this:
70
+ form.getAttribute(Fields.CustomerId).getValue()[0].id.replace('{','')
71
+ // CORRECT:
72
+ formLookupId(form.getAttribute(Fields.CustomerId))
73
+ ```
60
74
 
61
75
  6. **select()** from @xrmforge/helpers for ALL $select queries:
62
76
  ```typescript
63
77
  import { select } from '@xrmforge/helpers';
78
+ // Simple query:
64
79
  Xrm.WebApi.retrieveRecord(EntityNames.Account, id, select(Fields.Name, Fields.Revenue))
80
+ // Combined with $filter:
81
+ Xrm.WebApi.retrieveMultipleRecords(EntityNames.Account,
82
+ `${select(Fields.Name, Fields.Revenue)}&$filter=statecode eq 0`)
83
+ ```
84
+ **Signature:** `select(...fields: string[])` takes REST parameters, NOT an array.
85
+ ```typescript
86
+ select(Fields.Name, Fields.Revenue) // CORRECT
87
+ select([Fields.Name, Fields.Revenue]) // WRONG - array argument
88
+ select('account', Fields.Name) // WRONG - first arg is not entity name
65
89
  ```
66
90
 
67
91
  7. **wrapHandler()** around EVERY exported async event handler:
@@ -98,11 +122,26 @@ Run `xrmforge generate` to create:
98
122
  - Never `window.X = ...` (use module exports)
99
123
  - Never `console.log/warn/error` in form scripts (use shared logger)
100
124
  - Never export async handlers without wrapHandler()
101
- - Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames)
125
+ - Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames, no exceptions even for system entities)
102
126
  - Never `"?$select=name,revenue"` as raw string (use select() from @xrmforge/helpers)
103
- - Never `.getValue()[0].id.replace(...)` for lookups (use formLookup/formLookupId from @xrmforge/helpers)
127
+ - Never `.getValue()[0].id` or `.getValue()[0].id.replace(...)` for lookups (use formLookup/formLookupId from @xrmforge/helpers)
128
+ - Never build your own lookup helper (getLookupValueId, firstLookupValue, etc.) when formLookup/formLookupId exists
104
129
  - Never `import ... from '@xrmforge/typegen'` in browser code. @xrmforge/typegen is a Node.js CLI tool. Use `@xrmforge/helpers` for browser-safe runtime functions (select, parseLookup, formLookup, createUnboundAction, etc.)
105
130
 
131
+ ## Subagent Handoff (when delegating to sub-agents)
132
+
133
+ Copy these MANDATORY rules into every sub-agent prompt:
134
+
135
+ ```
136
+ 1. Fields Enum for ALL getAttribute/getControl (never raw strings)
137
+ 2. OptionSet Enum for ALL value comparisons (never magic numbers)
138
+ 3. EntityNames for ALL Xrm.WebApi calls (never raw entity names, no exceptions)
139
+ 4. formLookup/formLookupId for ALL lookup access (never .getValue()[0].id)
140
+ 5. select() for ALL $select queries (never raw "$select=" strings)
141
+ 6. wrapHandler() around EVERY exported async handler
142
+ 7. createLogger() instead of console.* (except logger.ts)
143
+ ```
144
+
106
145
  ## Mandatory Shared Utilities
107
146
 
108
147
  Every XrmForge project MUST have these in `src/shared/`:
@@ -146,16 +185,30 @@ import { StatusCode } from '../generated/optionsets/invoice.js';
146
185
  if (status.getValue() === StatusCode.Gebucht) { ... }
147
186
  ```
148
187
 
149
- ### Testing
188
+ ### Testing (onLoad + onChange)
150
189
  ```typescript
151
- import { createFormMock } from '@xrmforge/testing';
152
- const mock = createFormMock<AccountMainForm>({
153
- name: 'Test', statuscode: 0
154
- });
190
+ import { createFormMock, setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
191
+
192
+ beforeEach(() => setupXrmMock());
193
+ afterEach(() => teardownXrmMock());
194
+
195
+ // onLoad test:
196
+ const mock = createFormMock<AccountMainForm>({ name: 'Test', statuscode: 0 });
155
197
  onLoad(mock.asEventContext());
156
- expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
198
+ expect(mock.getControl(Fields.Revenue).getVisible()).toBe(true);
199
+
200
+ // onChange test (MANDATORY for every onChange handler):
201
+ mock.setValue(Fields.Revenue, 500000);
202
+ mock.fireOnChange(Fields.Revenue);
203
+ expect(mock.getControl(Fields.CreditLimit).getVisible()).toBe(true);
204
+ expect(mock.getValue(Fields.CreditOnHold)).toBe(true);
157
205
  ```
158
206
 
207
+ **Test quality rule:** At least 30% of tests MUST use `fireOnChange` or WebApi mock
208
+ assertions. Pure smoke tests (`onLoad` + `not.toThrow`) do NOT count as behavior tests.
209
+ Tests that only check `getOnChangeHandlers().length > 0` are registration tests, not
210
+ behavior tests. Every onChange handler MUST have a `fireOnChange` test.
211
+
159
212
  ## File Structure
160
213
 
161
214
  ```
@@ -177,7 +230,7 @@ When you see these patterns in legacy code, apply the XrmForge replacement:
177
230
  | `getValue() === 595300000` | `getValue() === OptionSets.StatusCode.Active` |
178
231
  | `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
179
232
  | `"?$select=name,revenue"` | `select(Fields.Name, Fields.Revenue)` (from @xrmforge/helpers) |
180
- | `value[0].id.replace("{","")...` | `parseLookup(form.getAttribute(Fields.X))` (from @xrmforge/helpers) |
233
+ | `value[0].id.replace("{","")...` | `formLookupId(form.getAttribute(Fields.X))` for forms, `parseLookup(response, 'field')` for Web API |
181
234
  | `Xrm.Page.getAttribute(...)` | `formContext.getAttribute(...)` |
182
235
  | `var formContext` (global) | `const form = ctx.getFormContext()` (parameter) |
183
236
  | `function form_OnLoad(ctx)` | `export function onLoad(ctx: Xrm.Events.EventContext)` |
@@ -273,8 +326,9 @@ grep -rn "getValue() ===" src/ --include="*.ts" | grep -E "[0-9]{3,}"
273
326
  # 3. Direct _value access instead of parseLookup (in Web API responses)
274
327
  grep -rn "_value\b" src/ --include="*.ts" | grep -v "generated/" | grep -v "parseLookup" | grep -v "getValue"
275
328
 
276
- # 4. Raw entity names in WebApi calls (must use EntityNames)
329
+ # 4. Raw entity names in WebApi calls (must use EntityNames, no exceptions)
277
330
  grep -rn "retrieveRecord\|retrieveMultipleRecords\|deleteRecord\|createRecord\|updateRecord" src/ --include="*.ts" | grep "'[a-z]" | grep -v "EntityNames"
331
+ # Every match is a violation. If entity is missing from EntityNames, extend generation.
278
332
 
279
333
  # 5. Missing select() - no raw "$select=" strings anywhere in src/
280
334
  grep -rn '\$select' src/ --include="*.ts" | grep -v "select(" | grep -v "generated/"
@@ -293,11 +347,11 @@ grep -rn "from.*generated/fields/" src/ --include="*.ts" | wc -l
293
347
  ### Code Quality (all must be 0)
294
348
 
295
349
  ```bash
296
- # console.* outside logger.ts
297
- grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts"
350
+ # console.* outside logger.ts (exclude JSDoc comments)
351
+ grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts" | grep -v "^\s*\*" | grep -v "^\s*//"
298
352
 
299
- # Xrm.Page (deprecated since D365 v9.0)
300
- grep -rn "Xrm\.Page" src/ --include="*.ts"
353
+ # Xrm.Page (deprecated since D365 v9.0, exclude JSDoc comments)
354
+ grep -rn "Xrm\.Page" src/ --include="*.ts" | grep -v "^\s*\*" | grep -v "^\s*//"
301
355
 
302
356
  # var declarations
303
357
  grep -rnE "^\s*var " src/ --include="*.ts"
@@ -57,10 +57,10 @@ echo ""
57
57
  echo "--- Code Quality ---"
58
58
 
59
59
  check "console.* outside logger.ts" \
60
- bash -c 'grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts"'
60
+ bash -c 'grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts" | grep -v "^\s*\*" | grep -v "^\s*//"'
61
61
 
62
62
  check "Xrm.Page (deprecated since D365 v9.0)" \
63
- bash -c 'grep -rn "Xrm\.Page" src/ --include="*.ts"'
63
+ bash -c 'grep -rn "Xrm\.Page" src/ --include="*.ts" | grep -v "^\s*\*" | grep -v "^\s*//"'
64
64
 
65
65
  check "var declarations" \
66
66
  bash -c 'grep -rnE "^\s*var " src/ --include="*.ts"'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrmforge/devkit",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Build orchestration and project tooling for Dynamics 365 WebResources",
5
5
  "keywords": [
6
6
  "dynamics-365",