@xrmforge/typegen 0.11.1 → 0.12.1

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 XrmForge Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XrmForge Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.ts CHANGED
@@ -489,6 +489,10 @@ interface SystemFormMetadata {
489
489
  formxml: string;
490
490
  description: string | null;
491
491
  isdefault: boolean;
492
+ /** Form type (systemform_type): 2 = Main, 7 = Quick Create, ... */
493
+ type: number;
494
+ /** Activation state (systemform_formactivationstate): 0 = Inactive, 1 = Active */
495
+ formactivationstate: number;
492
496
  }
493
497
  /** Parsed data-bound control from FormXml (bound to an attribute) */
494
498
  interface FormControl {
@@ -541,6 +545,8 @@ interface ParsedForm {
541
545
  name: string;
542
546
  formId: string;
543
547
  isDefault: boolean;
548
+ /** Form type (systemform_type): 2 = Main, 7 = Quick Create */
549
+ type: number;
544
550
  tabs: FormTab[];
545
551
  /** All data-bound controls across all tabs/sections (flattened) */
546
552
  allControls: FormControl[];
@@ -712,10 +718,14 @@ declare class MetadataClient {
712
718
  */
713
719
  getStateAttributes(logicalName: string): Promise<StateAttributeMetadata[]>;
714
720
  /**
715
- * Get and parse Main forms (type=2) for an entity.
716
- * Returns parsed form structures with tabs, sections, and controls.
721
+ * Get and parse the form types relevant for type generation (Main type=2 and
722
+ * Quick Create type=7), restricted to ACTIVE forms (formactivationstate=1).
723
+ * Inactive forms are leftovers that no app surfaces, so they get no interface -
724
+ * this applies to both Main and Quick Create forms.
725
+ *
726
+ * Returns parsed form structures with tabs, sections, controls, and the form type.
717
727
  */
718
- getMainForms(logicalName: string): Promise<ParsedForm[]>;
728
+ getForms(logicalName: string): Promise<ParsedForm[]>;
719
729
  /**
720
730
  * Get a global OptionSet by its exact name.
721
731
  */
@@ -1199,17 +1209,15 @@ declare function disambiguateEnumMembers(members: Array<{
1199
1209
  /**
1200
1210
  * @xrmforge/typegen - Entity Interface Generator
1201
1211
  *
1202
- * Generates TypeScript declaration files (.d.ts) for Dataverse entity interfaces.
1212
+ * Generates Dataverse entity interfaces as a flat ES module (.ts file).
1203
1213
  * These interfaces represent the data types returned by the Web API.
1204
1214
  *
1205
- * Output pattern:
1215
+ * Output pattern (flat ES module, one file per entity):
1206
1216
  * ```typescript
1207
- * declare namespace XrmForge.Entities {
1208
- * interface Account {
1209
- * accountid: string | null;
1210
- * name: string | null;
1211
- * // ...
1212
- * }
1217
+ * export interface Account {
1218
+ * accountid: string;
1219
+ * name: string | null;
1220
+ * // ...
1213
1221
  * }
1214
1222
  * ```
1215
1223
  */
@@ -1224,7 +1232,7 @@ interface EntityGeneratorOptions {
1224
1232
  *
1225
1233
  * @param info - Complete entity metadata (from MetadataClient.getEntityTypeInfo)
1226
1234
  * @param options - Generator options
1227
- * @returns TypeScript declaration string (.d.ts content)
1235
+ * @returns TypeScript source string (flat ES module)
1228
1236
  */
1229
1237
  declare function generateEntityInterface(info: EntityTypeInfo, options?: EntityGeneratorOptions): string;
1230
1238
 
@@ -1235,13 +1243,11 @@ declare function generateEntityInterface(info: EntityTypeInfo, options?: EntityG
1235
1243
  * Uses const enum because D365 form scripts have no module system at runtime,
1236
1244
  * so enum values must be inlined at compile time.
1237
1245
  *
1238
- * Output pattern:
1246
+ * Output pattern (flat ES module, all OptionSets of an entity in one file):
1239
1247
  * ```typescript
1240
- * declare namespace XrmForge.OptionSets {
1241
- * const enum AccountCategoryCode {
1242
- * PreferredCustomer = 1,
1243
- * Standard = 2,
1244
- * }
1248
+ * export const enum AccountCategoryCode {
1249
+ * PreferredCustomer = 1,
1250
+ * Standard = 2,
1245
1251
  * }
1246
1252
  * ```
1247
1253
  */
@@ -1291,38 +1297,37 @@ declare function generateEntityOptionSets(picklistAttributes: Array<{
1291
1297
  * 4. Fields const enum: provides autocomplete with dual-language labels
1292
1298
  * 5. NO fallback getAttribute(name: string): unknown fields are compile errors
1293
1299
  *
1294
- * Output pattern:
1300
+ * Output pattern (flat ES module, one file per entity, all forms combined):
1295
1301
  * ```typescript
1296
- * declare namespace XrmForge.Forms.Account {
1297
- * type AccountMainFormFields = "name" | "telephone1" | "revenue";
1298
- *
1299
- * type AccountMainFormAttributeMap = {
1300
- * name: Xrm.Attributes.StringAttribute;
1301
- * telephone1: Xrm.Attributes.StringAttribute;
1302
- * revenue: Xrm.Attributes.NumberAttribute;
1303
- * };
1304
- *
1305
- * type AccountMainFormControlMap = {
1306
- * name: Xrm.Controls.StringControl;
1307
- * telephone1: Xrm.Controls.StringControl;
1308
- * revenue: Xrm.Controls.NumberControl;
1309
- * };
1310
- *
1311
- * const enum AccountMainFormFields {
1312
- * Name = 'name',
1313
- * Telephone1 = 'telephone1',
1314
- * Revenue = 'revenue',
1315
- * }
1316
- *
1317
- * interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
1318
- * getAttribute<K extends AccountMainFormFields>(name: K): AccountMainFormAttributeMap[K];
1319
- * getAttribute(index: number): Xrm.Attributes.Attribute;
1320
- * getAttribute(): Xrm.Attributes.Attribute[];
1321
- * getControl<K extends AccountMainFormFields>(name: K): AccountMainFormControlMap[K];
1322
- * getControl(index: number): Xrm.Controls.Control;
1323
- * getControl(): Xrm.Controls.Control[];
1324
- * }
1302
+ * export type AccountMainFormFields = "name" | "telephone1" | "revenue";
1303
+ *
1304
+ * export type AccountMainFormAttributeMap = {
1305
+ * name: Xrm.Attributes.StringAttribute;
1306
+ * telephone1: Xrm.Attributes.StringAttribute;
1307
+ * revenue: Xrm.Attributes.NumberAttribute;
1308
+ * };
1309
+ *
1310
+ * export type AccountMainFormControlMap = {
1311
+ * name: Xrm.Controls.StringControl;
1312
+ * telephone1: Xrm.Controls.StringControl;
1313
+ * revenue: Xrm.Controls.NumberControl;
1314
+ * };
1315
+ *
1316
+ * export const enum AccountMainFormFieldsEnum {
1317
+ * Name = 'name',
1318
+ * Telephone1 = 'telephone1',
1319
+ * Revenue = 'revenue',
1320
+ * }
1321
+ *
1322
+ * export interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
1323
+ * getAttribute<K extends AccountMainFormFields>(name: K): AccountMainFormAttributeMap[K];
1324
+ * getAttribute(index: number): Xrm.Attributes.Attribute;
1325
+ * getAttribute(): Xrm.Attributes.Attribute[];
1326
+ * getControl<K extends AccountMainFormFields>(name: K): AccountMainFormControlMap[K];
1327
+ * getControl(index: number): Xrm.Controls.Control;
1328
+ * getControl(): Xrm.Controls.Control[];
1325
1329
  * }
1330
+ * // plus ...FormTabs/...Sections/...FormSubgrids enums, ...FormTypeInfo, ...FormMockValues
1326
1331
  * ```
1327
1332
  */
1328
1333
 
@@ -1364,15 +1369,13 @@ declare function generateEntityForms(forms: ParsedForm[], entityLogicalName: str
1364
1369
  * Generates a const enum with ALL entity fields for use with Xrm.WebApi.
1365
1370
  * Unlike form-specific Fields enums, this contains every readable attribute.
1366
1371
  *
1367
- * Output pattern:
1372
+ * Output pattern (flat ES module):
1368
1373
  * ```typescript
1369
- * declare namespace XrmForge.Entities {
1370
- * const enum AccountFields {
1371
- * /** Account Name | Firmenname *\/
1372
- * Name = 'name',
1373
- * /** Main Phone | Haupttelefon *\/
1374
- * Telephone1 = 'telephone1',
1375
- * }
1374
+ * export const enum AccountFields {
1375
+ * /** Account Name | Firmenname *\/
1376
+ * Name = 'name',
1377
+ * /** Main Phone | Haupttelefon *\/
1378
+ * Telephone1 = 'telephone1',
1376
1379
  * }
1377
1380
  * ```
1378
1381
  */
@@ -1422,14 +1425,12 @@ declare function generateEntityNavigationProperties(info: EntityTypeInfo, option
1422
1425
  * Generates a single const enum with all entity logical names.
1423
1426
  * Eliminates raw strings in Xrm.WebApi calls.
1424
1427
  *
1425
- * Output pattern:
1428
+ * Output pattern (flat ES module):
1426
1429
  * ```typescript
1427
- * declare namespace XrmForge {
1428
- * const enum EntityNames {
1429
- * Account = 'account',
1430
- * Contact = 'contact',
1431
- * Lead = 'lead',
1432
- * }
1430
+ * export const enum EntityNames {
1431
+ * Account = 'account',
1432
+ * Contact = 'contact',
1433
+ * Lead = 'lead',
1433
1434
  * }
1434
1435
  * ```
1435
1436
  */
@@ -1447,24 +1448,20 @@ declare function generateEntityNamesEnum(entityNames: string[], _options?: Entit
1447
1448
  /**
1448
1449
  * @xrmforge/typegen - Action/Function Generator
1449
1450
  *
1450
- * Generates TypeScript files for type-safe Custom API execution:
1451
- * - .d.ts: Parameter/Response interfaces and executor types
1452
- * - .ts: Runtime modules that import factory functions from @xrmforge/helpers
1451
+ * Generates a flat ES module (.ts) per Custom API group: exported Param/Result
1452
+ * interfaces plus runtime executors that import factory functions from @xrmforge/helpers.
1453
1453
  *
1454
1454
  * Input: CustomApiTypeInfo[] (from fixture JSON or live Dataverse query)
1455
1455
  * Output: Grouped by entity (bound) or "global" (unbound)
1456
1456
  *
1457
- * @example Generated output for markant_NormalizePhone (unbound action):
1457
+ * @example Generated output for markant_NormalizePhone (unbound action) in actions/global.ts:
1458
1458
  * ```typescript
1459
- * // global.d.ts
1460
- * declare namespace XrmForge.Actions {
1461
- * interface NormalizePhoneParams { Input: string; AllowSuspicious?: boolean; }
1462
- * interface NormalizePhoneResult { Normalized: string; Status: number; Message: string; }
1463
- * }
1464
- *
1465
- * // global.ts
1466
1459
  * import { createUnboundAction } from '@xrmforge/helpers';
1467
- * export const NormalizePhone = createUnboundAction<...>('markant_NormalizePhone', { ... });
1460
+ *
1461
+ * export type NormalizePhoneParams = { Input: string; AllowSuspicious?: boolean; };
1462
+ * export type NormalizePhoneResult = { Normalized: string; Status: number; Message: string; };
1463
+ * export const NormalizePhone =
1464
+ * createUnboundAction<NormalizePhoneParams, NormalizePhoneResult>('markant_NormalizePhone', { ... });
1468
1465
  * ```
1469
1466
  */
1470
1467
 
@@ -1485,7 +1482,7 @@ interface ActionGeneratorOptions {
1485
1482
  declare function generateActionDeclarations(apis: CustomApiTypeInfo[], _isFunction: boolean, _entityName?: string, _options?: ActionGeneratorOptions): string;
1486
1483
  /**
1487
1484
  * Generate a single .ts file with both interfaces and runtime executors for a group of Custom APIs.
1488
- * Combines what was previously split into .d.ts (interfaces) and .ts (executors).
1485
+ * Emits the Param/Result interfaces and the runtime executors together in one .ts file.
1489
1486
  *
1490
1487
  * @param apis - Custom APIs to generate
1491
1488
  * @param isFunction - true for functions, false for actions
@@ -1517,7 +1514,7 @@ interface GenerateConfig {
1517
1514
  entities: string[];
1518
1515
  /** Solution unique names to discover entities automatically (merged with entities, deduplicated) */
1519
1516
  solutionNames?: string[];
1520
- /** Output directory for generated .d.ts files */
1517
+ /** Output directory for generated .ts files */
1521
1518
  outputDir: string;
1522
1519
  /** Label language configuration */
1523
1520
  labelConfig: LabelConfig;
package/dist/index.js CHANGED
@@ -774,6 +774,7 @@ function parseForm(form, parser = defaultXmlParser) {
774
774
  name: form.name,
775
775
  formId: form.formid,
776
776
  isDefault: form.isdefault,
777
+ type: form.type,
777
778
  tabs,
778
779
  allControls,
779
780
  allSpecialControls
@@ -918,13 +919,15 @@ function extractParameter(controlElement, paramName) {
918
919
  // src/metadata/client.ts
919
920
  var log4 = createLogger("metadata");
920
921
  var FORM_TYPE_MAIN = 2;
922
+ var FORM_TYPE_QUICK_CREATE = 7;
923
+ var FORM_ACTIVATION_ACTIVE = 1;
921
924
  var COMPONENT_TYPE_ENTITY = 1;
922
925
  function byUniqueName(a, b) {
923
926
  return a.uniquename < b.uniquename ? -1 : a.uniquename > b.uniquename ? 1 : 0;
924
927
  }
925
928
  var ENTITY_SELECT = "LogicalName,SchemaName,EntitySetName,DisplayName,PrimaryIdAttribute,PrimaryNameAttribute,OwnershipType,IsCustomEntity,LogicalCollectionName,MetadataId";
926
929
  var ATTRIBUTE_SELECT = "LogicalName,SchemaName,AttributeType,AttributeTypeName,DisplayName,IsPrimaryId,IsPrimaryName,RequiredLevel,IsValidForRead,IsValidForCreate,IsValidForUpdate,MetadataId";
927
- var FORM_SELECT = "name,formid,formxml,description,isdefault";
930
+ var FORM_SELECT = "name,formid,formxml,description,isdefault,type,formactivationstate";
928
931
  var MetadataClient = class {
929
932
  http;
930
933
  constructor(httpClient) {
@@ -1001,16 +1004,22 @@ var MetadataClient = class {
1001
1004
  }
1002
1005
  // ─── Form Metadata ────────────────────────────────────────────────────
1003
1006
  /**
1004
- * Get and parse Main forms (type=2) for an entity.
1005
- * Returns parsed form structures with tabs, sections, and controls.
1007
+ * Get and parse the form types relevant for type generation (Main type=2 and
1008
+ * Quick Create type=7), restricted to ACTIVE forms (formactivationstate=1).
1009
+ * Inactive forms are leftovers that no app surfaces, so they get no interface -
1010
+ * this applies to both Main and Quick Create forms.
1011
+ *
1012
+ * Returns parsed form structures with tabs, sections, controls, and the form type.
1006
1013
  */
1007
- async getMainForms(logicalName) {
1014
+ async getForms(logicalName) {
1008
1015
  const safeName = DataverseHttpClient.sanitizeIdentifier(logicalName);
1009
- log4.info(`Fetching Main forms for: ${safeName}`);
1016
+ log4.info(`Fetching active Main + Quick Create forms for: ${safeName}`);
1010
1017
  const forms = await this.http.getAll(
1011
- `/systemforms?$filter=objecttypecode eq '${safeName}' and type eq ${FORM_TYPE_MAIN}&$select=${FORM_SELECT}`
1018
+ `/systemforms?$filter=objecttypecode eq '${safeName}' and (type eq ${FORM_TYPE_MAIN} or type eq ${FORM_TYPE_QUICK_CREATE}) and formactivationstate eq ${FORM_ACTIVATION_ACTIVE}&$select=${FORM_SELECT}`
1012
1019
  );
1013
- log4.info(`Found ${forms.length} Main form(s) for "${safeName}"`);
1020
+ const mainCount = forms.filter((f) => f.type === FORM_TYPE_MAIN).length;
1021
+ const qcCount = forms.filter((f) => f.type === FORM_TYPE_QUICK_CREATE).length;
1022
+ log4.info(`Found ${mainCount} Main + ${qcCount} Quick Create active form(s) for "${safeName}"`);
1014
1023
  return forms.map((form) => {
1015
1024
  try {
1016
1025
  return parseForm(form);
@@ -1024,6 +1033,7 @@ var MetadataClient = class {
1024
1033
  name: form.name,
1025
1034
  formId: form.formid,
1026
1035
  isDefault: form.isdefault,
1036
+ type: form.type,
1027
1037
  tabs: [],
1028
1038
  allControls: [],
1029
1039
  allSpecialControls: []
@@ -1152,7 +1162,7 @@ var MetadataClient = class {
1152
1162
  this.getLookupAttributes(safeName),
1153
1163
  this.getStatusAttributes(safeName),
1154
1164
  this.getStateAttributes(safeName),
1155
- this.getMainForms(safeName),
1165
+ this.getForms(safeName),
1156
1166
  this.getRelationships(safeName)
1157
1167
  ]);
1158
1168
  const result = {
@@ -1936,7 +1946,14 @@ function generateEntityOptionSets(picklistAttributes, entityLogicalName, options
1936
1946
  return results;
1937
1947
  }
1938
1948
 
1949
+ // src/generators/string-escape.ts
1950
+ function singleQuoted(value) {
1951
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
1952
+ return `'${escaped}'`;
1953
+ }
1954
+
1939
1955
  // src/generators/form-generator.ts
1956
+ var FORM_TYPE_QUICK_CREATE2 = 7;
1940
1957
  function specialControlToXrmType(controlType) {
1941
1958
  switch (controlType) {
1942
1959
  case "subgrid":
@@ -2065,7 +2082,7 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2065
2082
  if (field.label) {
2066
2083
  lines.push(` /** ${field.label} */`);
2067
2084
  }
2068
- lines.push(` ${field.enumMemberName} = '${field.logicalName}',`);
2085
+ lines.push(` ${field.enumMemberName} = ${singleQuoted(field.logicalName)},`);
2069
2086
  }
2070
2087
  lines.push("}");
2071
2088
  lines.push("");
@@ -2092,7 +2109,7 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2092
2109
  if (tab.label) {
2093
2110
  lines.push(` /** ${tab.label} */`);
2094
2111
  }
2095
- lines.push(` ${tabMemberNames[i]} = '${tab.name}',`);
2112
+ lines.push(` ${tabMemberNames[i]} = ${singleQuoted(tab.name)},`);
2096
2113
  }
2097
2114
  lines.push("}");
2098
2115
  lines.push("");
@@ -2117,7 +2134,7 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2117
2134
  sCounter++;
2118
2135
  }
2119
2136
  usedSectionMembers.add(sectionMember);
2120
- lines.push(` ${sectionMember} = '${section.name}',`);
2137
+ lines.push(` ${sectionMember} = ${singleQuoted(section.name)},`);
2121
2138
  }
2122
2139
  lines.push("}");
2123
2140
  lines.push("");
@@ -2142,7 +2159,7 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2142
2159
  usedMembers.add(member);
2143
2160
  const label = sg.targetEntityType ? `Subgrid: ${sg.targetEntityType}` : `Subgrid`;
2144
2161
  lines.push(` /** ${label} */`);
2145
- lines.push(` ${member} = '${sg.id}',`);
2162
+ lines.push(` ${member} = ${singleQuoted(sg.id)},`);
2146
2163
  }
2147
2164
  lines.push("}");
2148
2165
  lines.push("");
@@ -2162,7 +2179,7 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2162
2179
  }
2163
2180
  usedMembers.add(member);
2164
2181
  lines.push(` /** Quick View */`);
2165
- lines.push(` ${member} = '${qv.id}',`);
2182
+ lines.push(` ${member} = ${singleQuoted(qv.id)},`);
2166
2183
  }
2167
2184
  lines.push("}");
2168
2185
  lines.push("");
@@ -2239,8 +2256,8 @@ function generateEntityForms(forms, entityLogicalName, attributes, options = {})
2239
2256
  const entityPascal = toPascalCase(entityLogicalName);
2240
2257
  const validForms = forms.filter((f) => f.allControls.length > 0);
2241
2258
  const baseNames = validForms.map((form) => {
2242
- const safeFormName = toSafeFormName(form.name);
2243
- return buildFormBaseName(entityPascal, safeFormName);
2259
+ const base = buildFormBaseName(entityPascal, toSafeFormName(form.name));
2260
+ return form.type === FORM_TYPE_QUICK_CREATE2 ? `${base}QuickCreate` : base;
2244
2261
  });
2245
2262
  const baseNameCounts = /* @__PURE__ */ new Map();
2246
2263
  for (const name of baseNames) {