@khester/create-dynamics-app 1.0.8 → 1.1.0

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.
Files changed (107) hide show
  1. package/bin/create-dynamics-app.js +1 -1
  2. package/dist/index.js +140 -15
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/consultingHelpers.d.ts +13 -0
  5. package/dist/utils/consultingHelpers.d.ts.map +1 -0
  6. package/dist/utils/consultingHelpers.js +569 -0
  7. package/dist/utils/consultingHelpers.js.map +1 -0
  8. package/dist/utils/copyTemplate.d.ts.map +1 -1
  9. package/dist/utils/copyTemplate.js.map +1 -1
  10. package/dist/utils/initGit.d.ts.map +1 -1
  11. package/dist/utils/initGit.js.map +1 -1
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +3 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/dist/utils/updatePackageJson.d.ts +1 -1
  16. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  17. package/dist/utils/updatePackageJson.js +11 -1
  18. package/dist/utils/updatePackageJson.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
  21. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
  22. package/templates/dynamics-365-starter/README.md +566 -137
  23. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
  24. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
  25. package/templates/dynamics-365-starter/deployment/README.md +484 -0
  26. package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
  27. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
  28. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
  29. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
  30. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
  31. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
  32. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
  33. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
  34. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
  35. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
  36. package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
  37. package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
  38. package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
  39. package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
  40. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
  41. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
  42. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
  43. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
  44. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
  45. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
  46. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
  47. package/templates/dynamics-365-starter/package.json +22 -1
  48. package/templates/dynamics-365-starter/public/index.html +8 -11
  49. package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
  50. package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
  51. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
  52. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
  53. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
  54. package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
  55. package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
  56. package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
  57. package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
  58. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
  59. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
  60. package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
  61. package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
  62. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
  63. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
  64. package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
  65. package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
  66. package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
  67. package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
  68. package/templates/dynamics-365-starter/src/examples/README.md +52 -0
  69. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
  70. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
  71. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
  72. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
  73. package/templates/dynamics-365-starter/src/index.tsx +107 -19
  74. package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
  75. package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
  76. package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
  77. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
  78. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
  79. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
  80. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
  81. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
  82. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
  83. package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
  84. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
  85. package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
  86. package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
  87. package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
  88. package/templates/dynamics-365-starter/src/styles/index.css +74 -7
  89. package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
  90. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
  91. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
  92. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
  93. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
  94. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
  95. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
  96. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
  97. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
  98. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
  99. package/templates/dynamics-365-starter/tsconfig.json +11 -8
  100. package/templates/dynamics-365-starter/webpack.config.js +8 -9
  101. package/templates/power-pages-starter/README.md +7 -1
  102. package/templates/power-pages-starter/public/index.html +8 -11
  103. package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
  104. package/templates/power-pages-starter/src/index.tsx +3 -3
  105. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
  106. package/templates/power-pages-starter/tsconfig.json +3 -9
  107. package/templates/power-pages-starter/webpack.config.js +8 -3
@@ -0,0 +1,675 @@
1
+ /**
2
+ * TypeScript Type Generator for Dynamics 365 Entities
3
+ * Converts D365 metadata to TypeScript interfaces and constants
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+
9
+ class TypeScriptGenerator {
10
+ constructor(options = {}) {
11
+ this.options = {
12
+ generateInterfaces: true,
13
+ generateConstants: true,
14
+ generateValidators: true,
15
+ generateHelpers: true,
16
+ strictNullChecks: true,
17
+ includeComments: true,
18
+ exportAll: true,
19
+ ...options,
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Generate all TypeScript files for an entity
25
+ */
26
+ async generateEntityFiles(entityMetadata, outputDir) {
27
+ const entityName = entityMetadata.LogicalName;
28
+ const results = {};
29
+
30
+ if (this.options.generateInterfaces) {
31
+ const interfaceContent = this.generateInterfaceFile(entityMetadata);
32
+ const interfacePath = path.join(outputDir, `${entityName}.interface.ts`);
33
+ await fs.writeFile(interfacePath, interfaceContent);
34
+ results.interface = interfacePath;
35
+ }
36
+
37
+ if (this.options.generateConstants) {
38
+ const constantsContent = this.generateConstantsFile(entityMetadata);
39
+ const constantsPath = path.join(outputDir, `${entityName}.constants.ts`);
40
+ await fs.writeFile(constantsPath, constantsContent);
41
+ results.constants = constantsPath;
42
+ }
43
+
44
+ if (this.options.generateValidators) {
45
+ const validatorContent = this.generateValidatorFile(entityMetadata);
46
+ const validatorPath = path.join(outputDir, `${entityName}.validator.ts`);
47
+ await fs.writeFile(validatorPath, validatorContent);
48
+ results.validator = validatorPath;
49
+ }
50
+
51
+ if (this.options.generateHelpers) {
52
+ const helperContent = this.generateHelperFile(entityMetadata);
53
+ const helperPath = path.join(outputDir, `${entityName}.helpers.ts`);
54
+ await fs.writeFile(helperPath, helperContent);
55
+ results.helpers = helperPath;
56
+ }
57
+
58
+ return results;
59
+ }
60
+
61
+ /**
62
+ * Generate TypeScript interface file
63
+ */
64
+ generateInterfaceFile(metadata) {
65
+ const interfaceName = `I${metadata.SchemaName}`;
66
+ const attributes = metadata.Attributes || [];
67
+
68
+ let content = this.generateFileHeader(metadata, 'Interface definitions');
69
+
70
+ // Import statements
71
+ content += this.generateImports(metadata);
72
+
73
+ // Main interface
74
+ content += this.generateMainInterface(interfaceName, attributes, metadata);
75
+
76
+ // Create interface (without system fields)
77
+ content += this.generateCreateInterface(
78
+ interfaceName,
79
+ attributes,
80
+ metadata
81
+ );
82
+
83
+ // Update interface (partial, without system fields)
84
+ content += this.generateUpdateInterface(
85
+ interfaceName,
86
+ attributes,
87
+ metadata
88
+ );
89
+
90
+ // Response interface (with OData annotations)
91
+ content += this.generateResponseInterface(
92
+ interfaceName,
93
+ attributes,
94
+ metadata
95
+ );
96
+
97
+ // Lookup interfaces for related entities
98
+ content += this.generateLookupInterfaces(metadata);
99
+
100
+ return content;
101
+ }
102
+
103
+ /**
104
+ * Generate constants file
105
+ */
106
+ generateConstantsFile(metadata) {
107
+ const className = `${metadata.SchemaName}Constants`;
108
+ const attributes = metadata.Attributes || [];
109
+
110
+ let content = this.generateFileHeader(metadata, 'Constants and metadata');
111
+
112
+ // Class definition
113
+ content += `export class ${className} {\n`;
114
+
115
+ // Entity metadata
116
+ content += this.generateEntityMetadata(metadata);
117
+
118
+ // Attribute constants
119
+ content += this.generateAttributeConstants(attributes);
120
+
121
+ // Option set constants
122
+ content += this.generateOptionSetConstants(attributes);
123
+
124
+ // Relationship constants
125
+ content += this.generateRelationshipConstants(metadata);
126
+
127
+ // Display names
128
+ content += this.generateDisplayNames(attributes);
129
+
130
+ // Required fields
131
+ content += this.generateRequiredFields(attributes);
132
+
133
+ // Field collections
134
+ content += this.generateFieldCollections(attributes);
135
+
136
+ content += '}\n';
137
+
138
+ return content;
139
+ }
140
+
141
+ /**
142
+ * Generate validator file
143
+ */
144
+ generateValidatorFile(metadata) {
145
+ const interfaceName = `I${metadata.SchemaName}`;
146
+ const validatorName = `${metadata.SchemaName}Validator`;
147
+ const attributes = metadata.Attributes || [];
148
+
149
+ let content = this.generateFileHeader(metadata, 'Validation functions');
150
+
151
+ // Import statements
152
+ content += `import { ${interfaceName}, ${interfaceName}Create, ${interfaceName}Update } from './${metadata.LogicalName}.interface';\n`;
153
+ content += `import { ${metadata.SchemaName}Constants } from './${metadata.LogicalName}.constants';\n\n`;
154
+
155
+ // Validation result interface
156
+ content += this.generateValidationResultInterface();
157
+
158
+ // Main validator class
159
+ content += this.generateValidatorClass(
160
+ validatorName,
161
+ interfaceName,
162
+ attributes,
163
+ metadata
164
+ );
165
+
166
+ return content;
167
+ }
168
+
169
+ /**
170
+ * Generate helper file
171
+ */
172
+ generateHelperFile(metadata) {
173
+ const interfaceName = `I${metadata.SchemaName}`;
174
+ const helperName = `${metadata.SchemaName}Helper`;
175
+
176
+ let content = this.generateFileHeader(
177
+ metadata,
178
+ 'Helper functions and utilities'
179
+ );
180
+
181
+ // Import statements
182
+ content += `import { ${interfaceName} } from './${metadata.LogicalName}.interface';\n`;
183
+ content += `import { ${metadata.SchemaName}Constants } from './${metadata.LogicalName}.constants';\n\n`;
184
+
185
+ // Helper class
186
+ content += this.generateHelperClass(helperName, interfaceName, metadata);
187
+
188
+ return content;
189
+ }
190
+
191
+ /**
192
+ * Generate file header with metadata
193
+ */
194
+ generateFileHeader(metadata, description) {
195
+ if (!this.options.includeComments) return '';
196
+
197
+ return `/**
198
+ * Auto-generated ${description} for ${metadata.DisplayName} entity
199
+ *
200
+ * Entity: ${metadata.LogicalName} (${metadata.SchemaName})
201
+ * Generated: ${new Date().toISOString()}
202
+ * Source: ${metadata.Metadata?.SyncedFrom || 'Dynamics 365'}
203
+ *
204
+ * @warning This file is auto-generated. Do not modify manually.
205
+ * Re-run the metadata sync to update this file.
206
+ */
207
+
208
+ `;
209
+ }
210
+
211
+ /**
212
+ * Generate import statements
213
+ */
214
+ generateImports(metadata) {
215
+ let imports = '';
216
+
217
+ // Check if we need Date type imports
218
+ const hasDateFields = metadata.Attributes?.some(
219
+ (attr) => attr.AttributeType === 'DateTime'
220
+ );
221
+ if (hasDateFields) {
222
+ imports += '// Note: Date fields are represented as Date objects\n';
223
+ }
224
+
225
+ // Check if we need lookup type imports
226
+ const hasLookupFields = metadata.Attributes?.some((attr) =>
227
+ ['Lookup', 'Customer', 'Owner'].includes(attr.AttributeType)
228
+ );
229
+ if (hasLookupFields) {
230
+ imports += '// Lookup fields reference related entity IDs\n';
231
+ }
232
+
233
+ if (imports) {
234
+ imports += '\n';
235
+ }
236
+
237
+ return imports;
238
+ }
239
+
240
+ /**
241
+ * Generate main interface
242
+ */
243
+ generateMainInterface(interfaceName, attributes, metadata) {
244
+ let content = `export interface ${interfaceName} {\n`;
245
+
246
+ attributes.forEach((attr) => {
247
+ const tsType = this.mapAttributeToTypeScript(attr);
248
+ const isOptional = this.isAttributeOptional(attr);
249
+ const optionalMarker = isOptional ? '?' : '';
250
+
251
+ if (this.options.includeComments) {
252
+ content += ` /** ${attr.DisplayName}`;
253
+ if (attr.AttributeType) content += ` - Type: ${attr.AttributeType}`;
254
+ if (attr.MaxLength) content += `, MaxLength: ${attr.MaxLength}`;
255
+ if (attr.RequiredLevel) content += `, Required: ${attr.RequiredLevel}`;
256
+ content += ' */\n';
257
+ }
258
+
259
+ content += ` ${attr.LogicalName}${optionalMarker}: ${tsType};\n\n`;
260
+ });
261
+
262
+ content += '}\n\n';
263
+ return content;
264
+ }
265
+
266
+ /**
267
+ * Generate create interface (omits system fields)
268
+ */
269
+ generateCreateInterface(interfaceName, attributes, metadata) {
270
+ const systemFields = [
271
+ metadata.PrimaryIdAttribute,
272
+ 'createdon',
273
+ 'modifiedon',
274
+ 'createdby',
275
+ 'modifiedby',
276
+ 'versionnumber',
277
+ 'ownerid', // Usually set by system
278
+ ];
279
+
280
+ const omitFields = systemFields
281
+ .filter((field) => attributes.some((attr) => attr.LogicalName === field))
282
+ .join(' | ');
283
+
284
+ let content = `export interface ${interfaceName}Create`;
285
+
286
+ if (omitFields) {
287
+ content += ` extends Omit<${interfaceName}, '${omitFields.replace(' | ', "' | '")}'`;
288
+ } else {
289
+ content += ` extends ${interfaceName}`;
290
+ }
291
+
292
+ content += ' {}\n\n';
293
+ return content;
294
+ }
295
+
296
+ /**
297
+ * Generate update interface (partial)
298
+ */
299
+ generateUpdateInterface(interfaceName, attributes, metadata) {
300
+ const systemFields = [
301
+ metadata.PrimaryIdAttribute,
302
+ 'createdon',
303
+ 'modifiedon',
304
+ 'createdby',
305
+ 'modifiedby',
306
+ 'versionnumber',
307
+ ];
308
+
309
+ const omitFields = systemFields
310
+ .filter((field) => attributes.some((attr) => attr.LogicalName === field))
311
+ .join(' | ');
312
+
313
+ let content = `export interface ${interfaceName}Update extends Partial<`;
314
+
315
+ if (omitFields) {
316
+ content += `Omit<${interfaceName}, '${omitFields.replace(' | ', "' | '")}'>`;
317
+ } else {
318
+ content += interfaceName;
319
+ }
320
+
321
+ content += '> {}\n\n';
322
+ return content;
323
+ }
324
+
325
+ /**
326
+ * Generate response interface with OData annotations
327
+ */
328
+ generateResponseInterface(interfaceName, attributes, metadata) {
329
+ let content = `export interface ${interfaceName}Response extends ${interfaceName} {\n`;
330
+ content += ` '@odata.etag'?: string;\n`;
331
+ content += ` '@odata.context'?: string;\n`;
332
+
333
+ // Add navigation properties
334
+ const relationships = [
335
+ ...(metadata.OneToManyRelationships || []),
336
+ ...(metadata.ManyToOneRelationships || []),
337
+ ];
338
+
339
+ relationships.forEach((rel) => {
340
+ if (this.options.includeComments) {
341
+ content += ` /** Navigation property: ${rel.SchemaName} */\n`;
342
+ }
343
+ content += ` '${rel.SchemaName}'?: any; // Navigation property\n`;
344
+ });
345
+
346
+ content += '}\n\n';
347
+ return content;
348
+ }
349
+
350
+ /**
351
+ * Generate lookup interfaces
352
+ */
353
+ generateLookupInterfaces(metadata) {
354
+ const lookupFields =
355
+ metadata.Attributes?.filter((attr) =>
356
+ ['Lookup', 'Customer', 'Owner'].includes(attr.AttributeType)
357
+ ) || [];
358
+
359
+ if (lookupFields.length === 0) return '';
360
+
361
+ let content = '// Lookup field interfaces\n';
362
+
363
+ lookupFields.forEach((field) => {
364
+ const interfaceName = `${metadata.SchemaName}${field.SchemaName}Lookup`;
365
+
366
+ content += `export interface ${interfaceName} {\n`;
367
+ content += ` ${field.LogicalName}: string; // Entity ID\n`;
368
+ content += ` '${field.LogicalName}@OData.Community.Display.V1.FormattedValue'?: string; // Display name\n`;
369
+ content += ` '${field.LogicalName}@Microsoft.Dynamics.CRM.lookuplogicalname'?: string; // Entity type\n`;
370
+ content += '}\n\n';
371
+ });
372
+
373
+ return content;
374
+ }
375
+
376
+ /**
377
+ * Generate entity metadata constants
378
+ */
379
+ generateEntityMetadata(metadata) {
380
+ let content = ' // Entity Metadata\n';
381
+ content += ` public static readonly LogicalName = '${metadata.LogicalName}';\n`;
382
+ content += ` public static readonly SchemaName = '${metadata.SchemaName}';\n`;
383
+ content += ` public static readonly EntitySetName = '${metadata.EntitySetName}';\n`;
384
+ content += ` public static readonly DisplayName = '${metadata.DisplayName}';\n`;
385
+ content += ` public static readonly PrimaryIdAttribute = '${metadata.PrimaryIdAttribute}';\n`;
386
+ content += ` public static readonly PrimaryNameAttribute = '${metadata.PrimaryNameAttribute}';\n`;
387
+ content += ` public static readonly IsCustomEntity = ${metadata.IsCustomEntity};\n\n`;
388
+ return content;
389
+ }
390
+
391
+ /**
392
+ * Generate attribute constants
393
+ */
394
+ generateAttributeConstants(attributes) {
395
+ let content = ' // Attribute Names\n';
396
+
397
+ attributes.forEach((attr) => {
398
+ if (this.options.includeComments) {
399
+ content += ` /** ${attr.DisplayName} - Type: ${attr.AttributeType}`;
400
+ if (attr.RequiredLevel) content += `, Required: ${attr.RequiredLevel}`;
401
+ if (attr.MaxLength) content += `, MaxLength: ${attr.MaxLength}`;
402
+ content += ' */\n';
403
+ }
404
+ content += ` public static readonly ${attr.SchemaName} = '${attr.LogicalName}';\n`;
405
+ });
406
+
407
+ content += '\n';
408
+ return content;
409
+ }
410
+
411
+ /**
412
+ * Generate option set constants
413
+ */
414
+ generateOptionSetConstants(attributes) {
415
+ const optionSetFields = attributes.filter((attr) =>
416
+ ['Picklist', 'State', 'Status', 'MultiSelectPicklist'].includes(
417
+ attr.AttributeType
418
+ )
419
+ );
420
+
421
+ if (optionSetFields.length === 0) return '';
422
+
423
+ let content = ' // Option Set Values\n';
424
+
425
+ optionSetFields.forEach((field) => {
426
+ content += ` public static readonly ${field.SchemaName}Options = {\n`;
427
+ content += ` // TODO: Add actual option values from metadata\n`;
428
+ content += ` // Sync with: npm run metadata:sync-optionsets\n`;
429
+ content += ' } as const;\n\n';
430
+ });
431
+
432
+ return content;
433
+ }
434
+
435
+ /**
436
+ * Generate relationship constants
437
+ */
438
+ generateRelationshipConstants(metadata) {
439
+ const relationships = [
440
+ ...(metadata.OneToManyRelationships || []),
441
+ ...(metadata.ManyToOneRelationships || []),
442
+ ];
443
+
444
+ if (relationships.length === 0) return '';
445
+
446
+ let content = ' // Relationship Names\n';
447
+
448
+ relationships.forEach((rel) => {
449
+ content += ` public static readonly ${rel.SchemaName} = '${rel.SchemaName}';\n`;
450
+ });
451
+
452
+ content += '\n';
453
+ return content;
454
+ }
455
+
456
+ /**
457
+ * Generate display names object
458
+ */
459
+ generateDisplayNames(attributes) {
460
+ let content = ' // Display Names for UI\n';
461
+ content += ' public static readonly FieldDisplayNames = {\n';
462
+
463
+ attributes.forEach((attr) => {
464
+ content += ` [this.${attr.SchemaName}]: '${attr.DisplayName}',\n`;
465
+ });
466
+
467
+ content += ' } as const;\n\n';
468
+ return content;
469
+ }
470
+
471
+ /**
472
+ * Generate required fields array
473
+ */
474
+ generateRequiredFields(attributes) {
475
+ const requiredFields = attributes.filter(
476
+ (attr) =>
477
+ attr.RequiredLevel === 'ApplicationRequired' ||
478
+ attr.RequiredLevel === 'SystemRequired'
479
+ );
480
+
481
+ let content = ' // Required Fields\n';
482
+ content += ' public static readonly RequiredFields = [\n';
483
+
484
+ requiredFields.forEach((field) => {
485
+ content += ` this.${field.SchemaName},\n`;
486
+ });
487
+
488
+ content += ' ] as const;\n\n';
489
+ return content;
490
+ }
491
+
492
+ /**
493
+ * Generate field collections
494
+ */
495
+ generateFieldCollections(attributes) {
496
+ let content = ' // Field Collections for different operations\n';
497
+
498
+ // Create fields (exclude system fields)
499
+ const createFields = attributes.filter(
500
+ (attr) => attr.IsValidForCreate && !attr.IsPrimaryId
501
+ );
502
+ content += ' public static readonly CreateFields = [\n';
503
+ createFields.forEach((field) => {
504
+ content += ` this.${field.SchemaName},\n`;
505
+ });
506
+ content += ' ] as const;\n\n';
507
+
508
+ // Update fields
509
+ const updateFields = attributes.filter((attr) => attr.IsValidForUpdate);
510
+ content += ' public static readonly UpdateFields = [\n';
511
+ updateFields.forEach((field) => {
512
+ content += ` this.${field.SchemaName},\n`;
513
+ });
514
+ content += ' ] as const;\n\n';
515
+
516
+ return content;
517
+ }
518
+
519
+ /**
520
+ * Generate validation result interface
521
+ */
522
+ generateValidationResultInterface() {
523
+ return `export interface ValidationResult {
524
+ isValid: boolean;
525
+ errors: ValidationError[];
526
+ }
527
+
528
+ export interface ValidationError {
529
+ field: string;
530
+ message: string;
531
+ value?: any;
532
+ }
533
+
534
+ `;
535
+ }
536
+
537
+ /**
538
+ * Generate validator class
539
+ */
540
+ generateValidatorClass(validatorName, interfaceName, attributes, metadata) {
541
+ let content = `export class ${validatorName} {\n`;
542
+
543
+ // Validate create method
544
+ content += ` public static validateCreate(data: ${interfaceName}Create): ValidationResult {\n`;
545
+ content += ' const errors: ValidationError[] = [];\n\n';
546
+
547
+ // Required field validation
548
+ const requiredFields = attributes.filter(
549
+ (attr) =>
550
+ attr.RequiredLevel === 'ApplicationRequired' && attr.IsValidForCreate
551
+ );
552
+
553
+ requiredFields.forEach((field) => {
554
+ content += ` if (!data.${field.LogicalName}) {\n`;
555
+ content += ` errors.push({\n`;
556
+ content += ` field: '${field.LogicalName}',\n`;
557
+ content += ` message: '${field.DisplayName} is required',\n`;
558
+ content += ` value: data.${field.LogicalName}\n`;
559
+ content += ' });\n';
560
+ content += ' }\n\n';
561
+ });
562
+
563
+ // String length validation
564
+ const stringFields = attributes.filter(
565
+ (attr) => attr.AttributeType === 'String' && attr.MaxLength
566
+ );
567
+
568
+ stringFields.forEach((field) => {
569
+ content += ` if (data.${field.LogicalName} && data.${field.LogicalName}.length > ${field.MaxLength}) {\n`;
570
+ content += ` errors.push({\n`;
571
+ content += ` field: '${field.LogicalName}',\n`;
572
+ content += ` message: '${field.DisplayName} cannot exceed ${field.MaxLength} characters',\n`;
573
+ content += ` value: data.${field.LogicalName}\n`;
574
+ content += ' });\n';
575
+ content += ' }\n\n';
576
+ });
577
+
578
+ content += ' return { isValid: errors.length === 0, errors };\n';
579
+ content += ' }\n\n';
580
+
581
+ // Validate update method
582
+ content += ` public static validateUpdate(data: ${interfaceName}Update): ValidationResult {\n`;
583
+ content += ' const errors: ValidationError[] = [];\n\n';
584
+ content += ' // Add update-specific validation logic here\n\n';
585
+ content += ' return { isValid: errors.length === 0, errors };\n';
586
+ content += ' }\n';
587
+
588
+ content += '}\n\n';
589
+ return content;
590
+ }
591
+
592
+ /**
593
+ * Generate helper class
594
+ */
595
+ generateHelperClass(helperName, interfaceName, metadata) {
596
+ let content = `export class ${helperName} {\n`;
597
+
598
+ // Format display name method
599
+ content += ` public static getDisplayName(entity: ${interfaceName}): string {\n`;
600
+ content += ` return entity.${metadata.PrimaryNameAttribute} || 'Unnamed ${metadata.DisplayName}';\n`;
601
+ content += ' }\n\n';
602
+
603
+ // Convert to display object method
604
+ content += ` public static toDisplayObject(entity: ${interfaceName}): Record<string, string> {\n`;
605
+ content += ' return {\n';
606
+ content += ` id: entity.${metadata.PrimaryIdAttribute} || '',\n`;
607
+ content += ` name: this.getDisplayName(entity),\n`;
608
+ content += ' // Add other commonly displayed fields here\n';
609
+ content += ' };\n';
610
+ content += ' }\n\n';
611
+
612
+ // Convert to create data method
613
+ content += ` public static toCreateData(entity: ${interfaceName}): ${interfaceName}Create {\n`;
614
+ content += ' const createData: any = {};\n\n';
615
+ content += ` ${metadata.SchemaName}Constants.CreateFields.forEach(field => {\n`;
616
+ content += ' if (entity[field] !== undefined) {\n';
617
+ content += ' createData[field] = entity[field];\n';
618
+ content += ' }\n';
619
+ content += ' });\n\n';
620
+ content += ' return createData;\n';
621
+ content += ' }\n';
622
+
623
+ content += '}\n\n';
624
+ return content;
625
+ }
626
+
627
+ /**
628
+ * Map D365 attribute type to TypeScript type
629
+ */
630
+ mapAttributeToTypeScript(attribute) {
631
+ const typeMap = {
632
+ String: 'string',
633
+ Memo: 'string',
634
+ Integer: 'number',
635
+ BigInt: 'number',
636
+ Double: 'number',
637
+ Decimal: 'number',
638
+ Money: 'number',
639
+ Boolean: 'boolean',
640
+ DateTime: 'Date',
641
+ Uniqueidentifier: 'string',
642
+ Picklist: 'number',
643
+ State: 'number',
644
+ Status: 'number',
645
+ Lookup: 'string',
646
+ Customer: 'string',
647
+ Owner: 'string',
648
+ MultiSelectPicklist: 'number[]',
649
+ Virtual: 'any',
650
+ EntityName: 'string',
651
+ ManagedProperty: 'boolean',
652
+ };
653
+
654
+ const baseType = typeMap[attribute.AttributeType] || 'unknown';
655
+
656
+ // Add null union if nullable
657
+ if (this.options.strictNullChecks && this.isAttributeOptional(attribute)) {
658
+ return `${baseType} | null`;
659
+ }
660
+
661
+ return baseType;
662
+ }
663
+
664
+ /**
665
+ * Determine if attribute is optional
666
+ */
667
+ isAttributeOptional(attribute) {
668
+ return (
669
+ attribute.RequiredLevel !== 'ApplicationRequired' &&
670
+ attribute.RequiredLevel !== 'SystemRequired'
671
+ );
672
+ }
673
+ }
674
+
675
+ module.exports = { TypeScriptGenerator };
@@ -1,11 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "es5",
4
- "lib": [
5
- "dom",
6
- "dom.iterable",
7
- "es6"
8
- ],
4
+ "lib": ["dom", "dom.iterable", "es6"],
9
5
  "allowJs": true,
10
6
  "skipLibCheck": true,
11
7
  "esModuleInterop": true,
@@ -20,7 +16,14 @@
20
16
  "noEmit": true,
21
17
  "jsx": "react-jsx"
22
18
  },
23
- "include": [
24
- "src"
19
+ "include": ["src/**/*"],
20
+ "exclude": [
21
+ "node_modules",
22
+ "build",
23
+ "dist",
24
+ "tools/**/*.template.ts",
25
+ "tools/**/templates/**/*.ts",
26
+ "**/templates/**/*",
27
+ "**/*.template.ts"
25
28
  ]
26
- }
29
+ }