@xrmforge/devkit 0.7.3 → 0.7.5

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.
@@ -65,7 +65,7 @@ export const onLoad = wrapHandler('LM.Account.onLoad', logger, (ctx) => {
65
65
  const form = typedForm<AccountLMFirmaForm>(ctx.getFormContext());
66
66
 
67
67
  // Direct field access - fully typed, IDE autocomplete works
68
- const name = form.name.getValue(); // string | null
68
+ const name = form.name.getValue(); // string | null (non-nullable, field is on form)
69
69
  form.revenue.setValue(150000); // NumberAttribute
70
70
  const parent = form.parentaccountid.getValue(); // LookupValue[] | null
71
71
 
@@ -85,8 +85,29 @@ export const onLoad = wrapHandler('LM.Account.onLoad', logger, (ctx) => {
85
85
 
86
86
  **When to use `form.fieldname` (the typedForm proxy):**
87
87
  - `getValue()`, `setValue()` (reading and writing values)
88
+ - `addOnChange()` (event registration directly on the attribute)
88
89
  - Any read-only access to field values
89
90
 
91
+ **When to use `form.$unsafe(EntityFields.X)` (off-form fields):**
92
+
93
+ D365 loads all entity attributes into the FormContext, not just those on the form.
94
+ If a field is NOT in the generated form interface (compile error on `form.fieldname`),
95
+ use `$unsafe()` with the **Entity-level Fields Enum** (never a raw string):
96
+
97
+ ```typescript
98
+ import { OpportunityFields } from '../../generated/fields/opportunity.js';
99
+
100
+ // Off-form field: not in the form interface, but loaded by D365
101
+ form.$unsafe(OpportunityFields.VslBeauftragung)?.setValue(closeDate);
102
+
103
+ // WRONG: raw string in $unsafe
104
+ form.$unsafe('estimatedclosedate')?.setValue(closeDate);
105
+ ```
106
+
107
+ `$unsafe()` returns `Attribute | null` (nullable, because the field may not exist).
108
+ Always use optional chaining (`?.`). The Entity-level Fields Enum ensures the field
109
+ name is valid even though it's not on the form.
110
+
90
111
  ### 2. Fields Enum for ALL getAttribute/getControl AND select() calls
91
112
 
92
113
  Two types of Fields enums exist:
@@ -334,6 +355,8 @@ Xrm.Navigation.openForm({ entityName: EntityNames.Account, entityId: id }); //
334
355
  - Never access WebApi response properties with `as string` casts (use generated Entity interfaces)
335
356
  - Never `.getValue()[0].id` for lookups (use `formLookup`/`formLookupId`)
336
357
  - Never raw strings in `parseLookup()` (use NavigationProperties enum)
358
+ - Never raw strings in `$unsafe()` (use Entity-level Fields Enum: `form.$unsafe(AccountFields.X)`)
359
+ - Never manual OData annotation access (`_value`, `@OData.Community.Display.V1.FormattedValue`, `@Microsoft.Dynamics.CRM.lookuplogicalname`). Use `parseLookup()` which extracts all three.
337
360
 
338
361
  **Code quality:**
339
362
  - Never `Xrm.Page` (deprecated since D365 v9.0)
@@ -438,13 +461,32 @@ import { LmBestellungStatusCode } from '../../generated/optionsets/lm_bestellung
438
461
  </condition>`
439
462
  ```
440
463
 
441
- ### Lookup Access
464
+ ### Lookup Access (Form)
442
465
  ```typescript
443
466
  // BEFORE: form.getAttribute("customerid").getValue()[0].id.replace("{","").replace("}","")
444
467
  // AFTER:
445
468
  const customerId = formLookupId(form.customerid);
446
469
  ```
447
470
 
471
+ ### Lookup from WebApi Response to Form Field
472
+ ```typescript
473
+ // BEFORE (verbose, error-prone OData annotations):
474
+ form.customerid.setValue([{
475
+ id: raw._parentcustomerid_value as string,
476
+ entityType: EntityNames.Account,
477
+ name: (raw['_parentcustomerid_value@OData.Community.Display.V1.FormattedValue'] as string) ?? '',
478
+ }]);
479
+
480
+ // AFTER (parseLookup extracts id, name, entityType automatically):
481
+ import { parseLookup } from '@xrmforge/helpers';
482
+ import { ContactNavigationProperties as ContactNav } from '../../generated/entities/contact.js';
483
+
484
+ const customer = parseLookup(raw, ContactNav.ParentCustomerId);
485
+ if (customer) {
486
+ form.customerid.setValue([customer]);
487
+ }
488
+ ```
489
+
448
490
  ### Custom API Call
449
491
  ```typescript
450
492
  // BEFORE: ExecuteFunctionCall("CancelInvoice", { InvoiceId: id })
@@ -218,6 +218,34 @@ checkPattern(
218
218
  ['logger.ts'],
219
219
  );
220
220
 
221
+ // ── Type Safety Bypass ───────────────────────────────────────────────────────
222
+
223
+ // 3l2. Cast to Xrm.FormContext (bypasses typed form interface)
224
+ checkPattern(
225
+ 'Cast to Xrm.FormContext (use typedForm $unsafe() for off-form fields)',
226
+ formFiles,
227
+ /as\s+(?:unknown\s+as\s+)?Xrm\.FormContext/,
228
+ );
229
+
230
+ // 3l3. Raw strings in $filter (field names must use Fields Enum interpolation)
231
+ checkPattern(
232
+ 'Raw field names in $filter (use Fields Enum interpolation)',
233
+ allSrcFiles,
234
+ /\$filter=[^$]*\b(?:eq|ne|gt|lt|ge|le|contains|startswith)\b/,
235
+ ['generated/', 'validate-form'],
236
+ [
237
+ // Allow if the line contains template literal interpolation (${...})
238
+ /\$\{/,
239
+ ],
240
+ );
241
+
242
+ // 3l4. Raw strings in $unsafe() (must use Entity-level Fields Enum)
243
+ checkPattern(
244
+ 'Raw field strings in $unsafe() (use Entity-level Fields Enum)',
245
+ formFiles,
246
+ /\$unsafe\s*\(\s*['"][a-z]/,
247
+ );
248
+
221
249
  // ── Handler Pattern ──────────────────────────────────────────────────────────
222
250
 
223
251
  // 3l. Exported handlers without wrapHandler or wrapCommand
@@ -260,6 +288,60 @@ checkPattern(
260
288
  ['generated/'],
261
289
  );
262
290
 
291
+ // ── WebApi Response Typing ────────────────────────────────────────────────────
292
+
293
+ // 3p. Untyped WebApi responses (must use generated Entity interfaces)
294
+ checkPattern(
295
+ 'Untyped WebApi response cast (use generated Entity interface instead of Record<string, unknown>)',
296
+ allSrcFiles,
297
+ /as\s+Record\s*<\s*string\s*,\s*unknown\s*>/,
298
+ ['generated/'],
299
+ );
300
+
301
+ // 3q. Manual OData annotation access (use parseLookup instead)
302
+ checkPattern(
303
+ 'Manual OData annotation access (use parseLookup() from @xrmforge/helpers)',
304
+ allSrcFiles,
305
+ /@OData\.Community\.Display|@Microsoft\.Dynamics\.CRM\.lookuplogicalname|_value(?:@|\s*as\s)/,
306
+ ['generated/', 'node_modules'],
307
+ );
308
+
309
+ // ── Legacy Helper Wrappers ───────────────────────────────────────────────────
310
+
311
+ // 3r. Forbidden legacy helper functions (must use typedForm + @xrmforge/helpers)
312
+ checkPattern(
313
+ 'Forbidden helper: getLookupId (use formLookupId from @xrmforge/helpers)',
314
+ allSrcFiles,
315
+ /\bgetLookupId\s*\(/,
316
+ ['generated/'],
317
+ );
318
+
319
+ checkPattern(
320
+ 'Forbidden helper: setLookupValue (use form.field.setValue([{...}]))',
321
+ allSrcFiles,
322
+ /\bsetLookupValue\s*\(/,
323
+ ['generated/'],
324
+ );
325
+
326
+ // ── UI Localization ──────────────────────────────────────────────────────────
327
+
328
+ // 3s. Hardcoded UI strings in dialogs/progress (must use pickLang from constants.ts)
329
+ checkPattern(
330
+ 'Hardcoded UI string in dialog/progress (use pickLang(MESSAGES) from constants.ts)',
331
+ allSrcFiles,
332
+ /(?:openAlertDialog|openConfirmDialog|openErrorDialog|showProgressIndicator)\s*\(\s*(?:\{\s*text\s*:\s*)?['"]/,
333
+ ['generated/', 'constants.ts'],
334
+ );
335
+
336
+ // ── Duplicate Framework Functions ────────────────────────────────────────────
337
+
338
+ // 3t. Own normalizeGuid/compareGuid (use normalizeGuid from @xrmforge/helpers)
339
+ checkPattern(
340
+ 'Own normalizeGuid/compareGuid definition (use normalizeGuid from @xrmforge/helpers)',
341
+ allSrcFiles,
342
+ /(?:export\s+)?function\s+(?:normalizeGuid|compareGuid)\s*\(/,
343
+ );
344
+
263
345
  // ============================================================
264
346
  // Result
265
347
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrmforge/devkit",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Build orchestration and project tooling for Dynamics 365 WebResources",
5
5
  "keywords": [
6
6
  "dynamics-365",