@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,65 @@
1
+ import { IApiService } from '@khester/dynamics-ui-api-client';
2
+ import { MockApiService } from './MockApiService';
3
+ import { XrmApiService } from './XrmApiService';
4
+
5
+ /**
6
+ * Factory class for creating API service instances based on the current environment.
7
+ * Automatically detects mock vs production environments and returns the appropriate service.
8
+ */
9
+ export class ServiceFactory {
10
+ /**
11
+ * Determines if the current environment is a mock/development environment.
12
+ * Checks for localhost or 127.0.0.1 hostnames.
13
+ */
14
+ public static get isMockEnvironment(): boolean {
15
+ const hostname = window.location.hostname;
16
+ return hostname === 'localhost' || hostname === '127.0.0.1';
17
+ }
18
+
19
+ /**
20
+ * Creates an API service instance appropriate for the current environment.
21
+ * @param Xrm The Xrm object from Dynamics 365 (required in production)
22
+ * @returns An IApiService implementation
23
+ * @throws Error if Xrm is not provided in production environment
24
+ */
25
+ public static createApiService(Xrm?: any): IApiService {
26
+ if (this.isMockEnvironment) {
27
+ console.log(
28
+ 'ServiceFactory: Running in mock environment - using MockApiService'
29
+ );
30
+ return new MockApiService();
31
+ }
32
+
33
+ if (!Xrm) {
34
+ throw new Error(
35
+ 'ServiceFactory: Xrm object is required in production environment. ' +
36
+ 'Please ensure this code is running within Dynamics 365 context.'
37
+ );
38
+ }
39
+
40
+ console.log(
41
+ 'ServiceFactory: Running in production environment - using XrmApiService'
42
+ );
43
+ return new XrmApiService(Xrm);
44
+ }
45
+
46
+ /**
47
+ * Gets the current environment type as a string.
48
+ * @returns 'mock' or 'production'
49
+ */
50
+ public static getEnvironmentType(): 'mock' | 'production' {
51
+ return this.isMockEnvironment ? 'mock' : 'production';
52
+ }
53
+
54
+ /**
55
+ * Checks if the code is running in a Dynamics 365 context.
56
+ * @returns True if Xrm global is available
57
+ */
58
+ public static isDynamics365Context(): boolean {
59
+ return (
60
+ typeof window !== 'undefined' &&
61
+ Object.prototype.hasOwnProperty.call(window, 'Xrm') &&
62
+ (window as any).Xrm !== undefined
63
+ );
64
+ }
65
+ }
@@ -0,0 +1,213 @@
1
+ import { IApiService } from '@khester/dynamics-ui-api-client';
2
+
3
+ /**
4
+ * Implementation of IApiService that uses Dynamics 365's Xrm.WebApi
5
+ * for direct integration with the platform.
6
+ */
7
+ export class XrmApiService implements IApiService {
8
+ private xrm: any;
9
+
10
+ constructor(xrm: any) {
11
+ if (!xrm || !xrm.WebApi) {
12
+ throw new Error('Valid Xrm object with WebApi is required');
13
+ }
14
+ this.xrm = xrm;
15
+ }
16
+
17
+ async createRecord(entityName: string, record: any): Promise<any> {
18
+ try {
19
+ console.log(`XrmApiService: Creating ${entityName} record`, record);
20
+
21
+ // Remove any system fields that shouldn't be in create payload
22
+ const createPayload = { ...record };
23
+ delete createPayload.createdon;
24
+ delete createPayload.modifiedon;
25
+ delete createPayload.createdby;
26
+ delete createPayload.modifiedby;
27
+
28
+ const result = await this.xrm.WebApi.createRecord(
29
+ entityName,
30
+ createPayload
31
+ );
32
+
33
+ // Xrm.WebApi.createRecord returns an object with id property
34
+ // We need to fetch the full record to return it
35
+ const createdRecord = await this.xrm.WebApi.retrieveRecord(
36
+ entityName,
37
+ result.id
38
+ );
39
+
40
+ return createdRecord;
41
+ } catch (error) {
42
+ console.error(`XrmApiService: Error creating ${entityName}`, error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ async updateRecord(
48
+ entityName: string,
49
+ id: string,
50
+ record: any
51
+ ): Promise<any> {
52
+ try {
53
+ console.log(`XrmApiService: Updating ${entityName} record ${id}`, record);
54
+
55
+ // Remove any system fields and primary key from update payload
56
+ const updatePayload = { ...record };
57
+ const primaryKey = this.getPrimaryKeyName(entityName);
58
+ delete updatePayload[primaryKey];
59
+ delete updatePayload.createdon;
60
+ delete updatePayload.modifiedon;
61
+ delete updatePayload.createdby;
62
+ delete updatePayload.modifiedby;
63
+
64
+ await this.xrm.WebApi.updateRecord(entityName, id, updatePayload);
65
+
66
+ // Fetch and return the updated record
67
+ const updatedRecord = await this.xrm.WebApi.retrieveRecord(
68
+ entityName,
69
+ id
70
+ );
71
+ return updatedRecord;
72
+ } catch (error) {
73
+ console.error(`XrmApiService: Error updating ${entityName}`, error);
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ async deleteRecord(entityName: string, id: string): Promise<void> {
79
+ try {
80
+ console.log(`XrmApiService: Deleting ${entityName} record ${id}`);
81
+ await this.xrm.WebApi.deleteRecord(entityName, id);
82
+ } catch (error) {
83
+ console.error(`XrmApiService: Error deleting ${entityName}`, error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ async retrieveMultipleRecords(
89
+ entityName: string,
90
+ fetchXml: string
91
+ ): Promise<{ entities: any[] }> {
92
+ try {
93
+ console.log(
94
+ `XrmApiService: Retrieving ${entityName} records with FetchXML`,
95
+ fetchXml
96
+ );
97
+
98
+ // Convert FetchXML to OData query if needed
99
+ // For now, we'll use Xrm.WebApi.retrieveMultipleRecords with options
100
+ const options = `?fetchXml=${encodeURIComponent(fetchXml)}`;
101
+
102
+ const result = await this.xrm.WebApi.retrieveMultipleRecords(
103
+ entityName,
104
+ options
105
+ );
106
+
107
+ return {
108
+ entities: result.entities || [],
109
+ };
110
+ } catch (error) {
111
+ console.error(
112
+ `XrmApiService: Error retrieving ${entityName} records`,
113
+ error
114
+ );
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ async executeRequest(requestName: string, requestData: any): Promise<any> {
120
+ try {
121
+ console.log(
122
+ `XrmApiService: Executing request ${requestName}`,
123
+ requestData
124
+ );
125
+
126
+ // Xrm.WebApi.execute expects a request object with specific structure
127
+ const request = {
128
+ ...requestData,
129
+ getMetadata: () => ({
130
+ boundParameter: null,
131
+ operationType: 0, // Action
132
+ operationName: requestName,
133
+ }),
134
+ };
135
+
136
+ const response = await this.xrm.WebApi.execute(request);
137
+
138
+ // Parse the response
139
+ if (response.ok) {
140
+ const responseData = await response.json();
141
+ return responseData;
142
+ } else {
143
+ throw new Error(`Request failed with status: ${response.status}`);
144
+ }
145
+ } catch (error) {
146
+ console.error(
147
+ `XrmApiService: Error executing request ${requestName}`,
148
+ error
149
+ );
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ async uploadFile(file: File): Promise<string> {
155
+ try {
156
+ console.log(`XrmApiService: Uploading file ${file.name}`);
157
+
158
+ // File upload in Dynamics 365 typically involves:
159
+ // 1. Creating an annotation (note) record
160
+ // 2. Attaching the file content as base64
161
+
162
+ // Convert file to base64
163
+ const base64Content = await this.fileToBase64(file);
164
+
165
+ // Create annotation record
166
+ const annotation = {
167
+ subject: file.name,
168
+ filename: file.name,
169
+ mimetype: file.type,
170
+ documentbody: base64Content,
171
+ isdocument: true,
172
+ };
173
+
174
+ const result = await this.xrm.WebApi.createRecord(
175
+ 'annotation',
176
+ annotation
177
+ );
178
+
179
+ // Return the annotation ID as the file reference
180
+ return result.id;
181
+ } catch (error) {
182
+ console.error(`XrmApiService: Error uploading file`, error);
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Helper method to convert File to base64 string
189
+ */
190
+ private fileToBase64(file: File): Promise<string> {
191
+ return new Promise((resolve, reject) => {
192
+ const reader = new FileReader();
193
+ reader.readAsDataURL(file);
194
+ reader.onload = () => {
195
+ // Remove data URL prefix to get just the base64 content
196
+ const base64 = (reader.result as string).split(',')[1];
197
+ resolve(base64);
198
+ };
199
+ reader.onerror = (error) => reject(error);
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Helper method to get the primary key name for an entity
205
+ */
206
+ private getPrimaryKeyName(entityName: string): string {
207
+ // Remove trailing 's' if present and add 'id'
208
+ const singularName = entityName.endsWith('s')
209
+ ? entityName.slice(0, -1)
210
+ : entityName;
211
+ return `${singularName}id`;
212
+ }
213
+ }
@@ -28,18 +28,57 @@ body {
28
28
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
29
29
  }
30
30
 
31
- .app-header h1 {
31
+ .app-header__content {
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ max-width: 1200px;
36
+ margin: 0 auto;
37
+ }
38
+
39
+ .app-header__title h1 {
32
40
  margin: 0 0 4px 0;
33
41
  font-size: 24px;
34
42
  font-weight: 600;
35
43
  }
36
44
 
37
- .app-header p {
45
+ .app-header__title p {
38
46
  margin: 0;
39
47
  font-size: 14px;
40
48
  opacity: 0.9;
41
49
  }
42
50
 
51
+ .app-header__actions {
52
+ display: flex;
53
+ gap: 12px;
54
+ align-items: center;
55
+ }
56
+
57
+ .debug-toggle-btn {
58
+ background-color: rgba(255, 255, 255, 0.1);
59
+ border: 1px solid rgba(255, 255, 255, 0.3);
60
+ color: white;
61
+ padding: 6px 12px;
62
+ border-radius: 4px;
63
+ cursor: pointer;
64
+ font-size: 12px;
65
+ transition: background-color 0.2s ease;
66
+ }
67
+
68
+ .debug-toggle-btn:hover {
69
+ background-color: rgba(255, 255, 255, 0.2);
70
+ }
71
+
72
+ .app-navigation {
73
+ background-color: white;
74
+ border-bottom: 1px solid #edebe9;
75
+ max-width: 1200px;
76
+ margin: 0 auto;
77
+ margin-top: 16px;
78
+ border-radius: 8px 8px 0 0;
79
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
80
+ }
81
+
43
82
  .app-main {
44
83
  flex: 1;
45
84
  padding: 0;
@@ -47,12 +86,25 @@ body {
47
86
  margin: 0 auto;
48
87
  width: 100%;
49
88
  background-color: white;
50
- margin-top: 16px;
51
89
  margin-bottom: 16px;
52
- border-radius: 8px;
90
+ border-radius: 0 0 8px 8px;
53
91
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
54
92
  }
55
93
 
94
+ .debug-panel-overlay {
95
+ position: fixed;
96
+ top: 0;
97
+ left: 0;
98
+ right: 0;
99
+ bottom: 0;
100
+ background-color: rgba(0, 0, 0, 0.5);
101
+ z-index: 1000;
102
+ display: flex;
103
+ justify-content: center;
104
+ align-items: center;
105
+ padding: 20px;
106
+ }
107
+
56
108
  .app-footer {
57
109
  background-color: #f3f2f1;
58
110
  border-top: 1px solid #edebe9;
@@ -93,12 +145,27 @@ body {
93
145
  padding: 12px 16px;
94
146
  }
95
147
 
96
- .app-header h1 {
148
+ .app-header__content {
149
+ flex-direction: column;
150
+ gap: 12px;
151
+ align-items: flex-start;
152
+ }
153
+
154
+ .app-header__title h1 {
97
155
  font-size: 20px;
98
156
  }
99
157
 
100
- .app-main {
158
+ .app-navigation {
101
159
  margin: 8px;
102
- border-radius: 4px;
160
+ border-radius: 4px 4px 0 0;
161
+ }
162
+
163
+ .app-main {
164
+ margin: 0 8px 8px 8px;
165
+ border-radius: 0 0 4px 4px;
166
+ }
167
+
168
+ .debug-panel-overlay {
169
+ padding: 10px;
103
170
  }
104
171
  }
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { program } = require('commander');
6
+
7
+ // Entity Generator CLI Tool for Dynamics 365
8
+ // Generates complete entity stack from templates
9
+
10
+ program
11
+ .name('entity-generator')
12
+ .description('Generate complete entity stack for Dynamics 365')
13
+ .version('1.0.0');
14
+
15
+ program
16
+ .command('generate')
17
+ .description('Generate entity files')
18
+ .requiredOption(
19
+ '-e, --entity <name>',
20
+ 'Entity logical name (e.g., customentity)'
21
+ )
22
+ .requiredOption(
23
+ '-d, --display-name <name>',
24
+ 'Entity display name (e.g., "Custom Entity")'
25
+ )
26
+ .option(
27
+ '-p, --plural <name>',
28
+ 'Entity plural name (defaults to entity + "s")'
29
+ )
30
+ .option('--output <path>', 'Output directory (defaults to src/)')
31
+ .action(generateEntity);
32
+
33
+ async function generateEntity(options) {
34
+ const { entity, displayName, plural, output } = options;
35
+
36
+ console.log(`🚀 Generating entity stack for: ${displayName} (${entity})`);
37
+
38
+ const config = {
39
+ entityName: entity.toLowerCase(),
40
+ entityNamePascal: toPascalCase(entity),
41
+ displayName,
42
+ pluralName: plural || entity.toLowerCase() + 's',
43
+ pluralNamePascal: toPascalCase(plural || entity + 's'),
44
+ outputDir: output || 'src',
45
+ };
46
+
47
+ try {
48
+ await generateEntityFiles(config);
49
+ console.log('✅ Entity stack generated successfully!');
50
+ console.log('\n📁 Generated files:');
51
+ console.log(` • src/models/${config.entityNamePascal}.ts`);
52
+ console.log(` • src/constants/${config.entityName}.ts`);
53
+ console.log(` • src/components/${config.entityNamePascal}Management.tsx`);
54
+ console.log(` • src/components/${config.entityNamePascal}Management.css`);
55
+ console.log(` • src/components/${config.entityNamePascal}Form.tsx`);
56
+ console.log(` • src/components/${config.entityNamePascal}Form.css`);
57
+ console.log('\n🎯 Next steps:');
58
+ console.log(' 1. Update your D365 field constants in the constants file');
59
+ console.log(' 2. Add the new components to your main application');
60
+ console.log(' 3. Run npm run build to compile');
61
+ } catch (error) {
62
+ console.error('❌ Error generating entity:', error.message);
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ async function generateEntityFiles(config) {
68
+ const templatesDir = path.join(__dirname, 'templates');
69
+ const projectRoot = findProjectRoot();
70
+ const outputDir = path.join(projectRoot, config.outputDir);
71
+
72
+ // Ensure output directories exist
73
+ ensureDirectoryExists(path.join(outputDir, 'models'));
74
+ ensureDirectoryExists(path.join(outputDir, 'constants'));
75
+ ensureDirectoryExists(path.join(outputDir, 'components'));
76
+
77
+ // Generate model file
78
+ await generateFromTemplate(
79
+ path.join(templatesDir, 'model.template.ts'),
80
+ path.join(outputDir, 'models', `${config.entityNamePascal}.ts`),
81
+ config
82
+ );
83
+
84
+ // Generate constants file
85
+ await generateFromTemplate(
86
+ path.join(templatesDir, 'constants.template.ts'),
87
+ path.join(outputDir, 'constants', `${config.entityName}.ts`),
88
+ config
89
+ );
90
+
91
+ // Generate management component
92
+ await generateFromTemplate(
93
+ path.join(templatesDir, 'management.template.tsx'),
94
+ path.join(
95
+ outputDir,
96
+ 'components',
97
+ `${config.entityNamePascal}Management.tsx`
98
+ ),
99
+ config
100
+ );
101
+
102
+ // Generate management CSS
103
+ await generateFromTemplate(
104
+ path.join(templatesDir, 'management.template.css'),
105
+ path.join(
106
+ outputDir,
107
+ 'components',
108
+ `${config.entityNamePascal}Management.css`
109
+ ),
110
+ config
111
+ );
112
+
113
+ // Generate form component
114
+ await generateFromTemplate(
115
+ path.join(templatesDir, 'form.template.tsx'),
116
+ path.join(outputDir, 'components', `${config.entityNamePascal}Form.tsx`),
117
+ config
118
+ );
119
+
120
+ // Generate form CSS
121
+ await generateFromTemplate(
122
+ path.join(templatesDir, 'form.template.css'),
123
+ path.join(outputDir, 'components', `${config.entityNamePascal}Form.css`),
124
+ config
125
+ );
126
+ }
127
+
128
+ async function generateFromTemplate(templatePath, outputPath, config) {
129
+ const template = fs.readFileSync(templatePath, 'utf8');
130
+ const generated = replaceTemplateVariables(template, config);
131
+ fs.writeFileSync(outputPath, generated);
132
+ }
133
+
134
+ function replaceTemplateVariables(template, config) {
135
+ return template
136
+ .replace(/\{\{entityName\}\}/g, config.entityName)
137
+ .replace(/\{\{entityNamePascal\}\}/g, config.entityNamePascal)
138
+ .replace(/\{\{displayName\}\}/g, config.displayName)
139
+ .replace(/\{\{pluralName\}\}/g, config.pluralName)
140
+ .replace(/\{\{pluralNamePascal\}\}/g, config.pluralNamePascal);
141
+ }
142
+
143
+ function toPascalCase(str) {
144
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
145
+ }
146
+
147
+ function ensureDirectoryExists(dirPath) {
148
+ if (!fs.existsSync(dirPath)) {
149
+ fs.mkdirSync(dirPath, { recursive: true });
150
+ }
151
+ }
152
+
153
+ function findProjectRoot() {
154
+ let dir = process.cwd();
155
+ while (dir !== '/') {
156
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
157
+ return dir;
158
+ }
159
+ dir = path.dirname(dir);
160
+ }
161
+ return process.cwd();
162
+ }
163
+
164
+ if (require.main === module) {
165
+ program.parse();
166
+ }
167
+
168
+ module.exports = { generateEntity };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * {{displayName}} Entity Constants
3
+ * Generated by Entity Generator
4
+ *
5
+ * Update these constants with actual field names from your Dynamics 365 entity
6
+ */
7
+ // Replace 'SampleEntity' with your actual entity PascalCase name during template processing
8
+ export class SampleEntityConstants {
9
+ /** Entity logical name */
10
+ public static readonly EntityLogicalName: string = "{{entityName}}";
11
+
12
+ /** Entity collection name for Web API */
13
+ public static readonly EntityCollectionName: string = "{{pluralName}}";
14
+
15
+ /** Primary ID attribute */
16
+ public static readonly PrimaryIdAttribute: string = "{{entityName}}id";
17
+
18
+ /** Primary name attribute */
19
+ public static readonly PrimaryNameAttribute: string = "{{entityName}}_name";
20
+
21
+ // System fields
22
+ /** Type: DateTime, ReadOnly: true */
23
+ public static readonly CreatedOn: string = "createdon";
24
+
25
+ /** Type: DateTime, ReadOnly: true */
26
+ public static readonly ModifiedOn: string = "modifiedon";
27
+
28
+ /** Type: Lookup, ReadOnly: true */
29
+ public static readonly CreatedBy: string = "createdby";
30
+
31
+ /** Type: Lookup, ReadOnly: true */
32
+ public static readonly ModifiedBy: string = "modifiedby";
33
+
34
+ /** Type: Lookup, ReadOnly: true */
35
+ public static readonly OwnerId: string = "ownerid";
36
+
37
+ /** Type: Picklist, ReadOnly: true */
38
+ public static readonly StateCode: string = "statecode";
39
+
40
+ /** Type: Picklist, ReadOnly: true */
41
+ public static readonly StatusCode: string = "statuscode";
42
+
43
+ // Custom fields - Update these with your actual field names
44
+ /** Type: String, RequiredLevel: ApplicationRequired, MaxLength: 100 */
45
+ public static readonly Description: string = "{{entityName}}_description";
46
+
47
+ /** Type: DateTime, RequiredLevel: None */
48
+ public static readonly DueDate: string = "{{entityName}}_duedate";
49
+
50
+ /** Type: Picklist, RequiredLevel: None */
51
+ public static readonly Priority: string = "{{entityName}}_priority";
52
+
53
+ /** Type: Lookup, RequiredLevel: None, Target: contact */
54
+ public static readonly ContactId: string = "{{entityName}}_contactid";
55
+
56
+ /** Type: Lookup, RequiredLevel: None, Target: account */
57
+ public static readonly AccountId: string = "{{entityName}}_accountid";
58
+
59
+ /** Type: Money, RequiredLevel: None */
60
+ public static readonly Amount: string = "{{entityName}}_amount";
61
+
62
+ /** Type: Boolean, RequiredLevel: None */
63
+ public static readonly IsActive: string = "{{entityName}}_isactive";
64
+
65
+ // Option Set Values - Update these with your actual option set values
66
+ public static readonly PriorityOptions = {
67
+ Low: 1,
68
+ Medium: 2,
69
+ High: 3,
70
+ Critical: 4
71
+ };
72
+
73
+ public static readonly StateCodeOptions = {
74
+ Active: 0,
75
+ Inactive: 1
76
+ };
77
+
78
+ public static readonly StatusCodeOptions = {
79
+ Active: 1,
80
+ Inactive: 2
81
+ };
82
+
83
+ // Field display names for UI
84
+ public static readonly FieldDisplayNames = {
85
+ [this.PrimaryNameAttribute]: "Name",
86
+ [this.Description]: "Description",
87
+ [this.DueDate]: "Due Date",
88
+ [this.Priority]: "Priority",
89
+ [this.ContactId]: "Contact",
90
+ [this.AccountId]: "Account",
91
+ [this.Amount]: "Amount",
92
+ [this.IsActive]: "Is Active",
93
+ [this.CreatedOn]: "Created On",
94
+ [this.ModifiedOn]: "Modified On",
95
+ [this.CreatedBy]: "Created By",
96
+ [this.ModifiedBy]: "Modified By"
97
+ };
98
+
99
+ // Required fields
100
+ public static readonly RequiredFields = [
101
+ this.PrimaryNameAttribute
102
+ ];
103
+
104
+ // Fields for list view
105
+ public static readonly ListViewColumns = [
106
+ this.PrimaryNameAttribute,
107
+ this.Description,
108
+ this.Priority,
109
+ this.DueDate,
110
+ this.CreatedOn
111
+ ];
112
+
113
+ // Fields for form view
114
+ public static readonly FormFields = [
115
+ this.PrimaryNameAttribute,
116
+ this.Description,
117
+ this.Priority,
118
+ this.DueDate,
119
+ this.ContactId,
120
+ this.AccountId,
121
+ this.Amount,
122
+ this.IsActive
123
+ ];
124
+ }