@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,250 @@
1
+ // Replace 'SampleEntity' with your actual entity PascalCase name during template processing
2
+ // import { BaseEntity } from './BaseEntity';
3
+ // import { IApiService } from '../services/IApiService';
4
+ // import { Logger } from '../components/Logging/logger';
5
+ // import { SampleEntityConstants } from '../constants/sample-entity';
6
+
7
+ // Template base entity class - replace with actual BaseEntity during generation
8
+ abstract class BaseEntity {
9
+ id?: string;
10
+ [key: string]: any;
11
+ }
12
+
13
+ // Template API service interface - replace with actual IApiService during generation
14
+ interface IApiService {
15
+ createRecord(entityName: string, data: any): Promise<any>;
16
+ updateRecord(entityName: string, id: string, data: any): Promise<any>;
17
+ deleteRecord(entityName: string, id: string): Promise<void>;
18
+ retrieveMultipleRecords(entityName: string, fetchXml: string): Promise<{ entities: any[] }>;
19
+ }
20
+
21
+ // Template Logger - replace with actual Logger during generation
22
+ const Logger = {
23
+ log: (message: string, source?: string) => console.log(message, source),
24
+ warn: (message: string, source?: string) => console.warn(message, source),
25
+ error: (message: string, source?: string, error?: any) => console.error(message, source, error)
26
+ };
27
+
28
+ // Template constants - replace with actual constants during generation
29
+ const SampleEntityConstants = {
30
+ EntityLogicalName: 'sample_entity',
31
+ PrimaryIdAttribute: 'sample_entityid',
32
+ PrimaryNameAttribute: 'name',
33
+ FieldDisplayNames: {
34
+ sample_entityid: 'ID',
35
+ name: 'Name'
36
+ }
37
+ };
38
+
39
+ export interface ISampleEntity {
40
+ sample_entityid: string;
41
+ name: string;
42
+ createdOn?: Date;
43
+ modifiedOn?: Date;
44
+ }
45
+
46
+ /**
47
+ * Sample Entity entity model with CRUD operations
48
+ * Generated by Entity Generator
49
+ */
50
+ export class SampleEntity extends BaseEntity implements ISampleEntity {
51
+ public sample_entityid: string = '';
52
+ public name: string = '';
53
+ public createdOn?: Date;
54
+ public modifiedOn?: Date;
55
+
56
+ constructor(data?: Partial<ISampleEntity>) {
57
+ super();
58
+ if (data) {
59
+ Object.assign(this, data);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Create a new Sample Entity record
65
+ */
66
+ public static async create(apiService: IApiService, sampleEntity: SampleEntity): Promise<SampleEntity> {
67
+ try {
68
+ Logger.log(`Creating new Sample Entity: ${sampleEntity.name}`);
69
+
70
+ const data = sampleEntity.toCreateData();
71
+ const result = await apiService.createRecord(SampleEntityConstants.EntityLogicalName, data);
72
+
73
+ const createdSampleEntity = new SampleEntity({
74
+ ...sampleEntity,
75
+ sample_entityid: result.id || result.sample_entityid
76
+ });
77
+
78
+ Logger.log(`Sample Entity created successfully with ID: ${createdSampleEntity.sample_entityid}`);
79
+ return createdSampleEntity;
80
+ } catch (error) {
81
+ Logger.error(`Error creating Sample Entity:`, error);
82
+ throw new Error(`Failed to create Sample Entity: ${error instanceof Error ? error.message : 'Unknown error'}`);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Retrieve Sample Entity by ID
88
+ */
89
+ public static async retrieveById(apiService: IApiService, id: string): Promise<SampleEntity | null> {
90
+ try {
91
+ Logger.log(`Retrieving Sample Entity with ID: ${id}`);
92
+
93
+ const fetchXml = `
94
+ <fetch top="1">
95
+ <entity name="${SampleEntityConstants.EntityLogicalName}">
96
+ <attribute name="sample_entityid" />
97
+ <attribute name="name" />
98
+ <attribute name="createdon" />
99
+ <attribute name="modifiedon" />
100
+ <filter>
101
+ <condition attribute="sample_entityid" operator="eq" value="${id}" />
102
+ </filter>
103
+ </entity>
104
+ </fetch>`;
105
+
106
+ const result = await apiService.retrieveMultipleRecords(SampleEntityConstants.EntityLogicalName, fetchXml);
107
+
108
+ if (result.entities && result.entities.length > 0) {
109
+ const entity = result.entities[0];
110
+ return new SampleEntity({
111
+ sample_entityid: entity.sample_entityid,
112
+ name: entity.name,
113
+ createdOn: entity.createdon ? new Date(entity.createdon) : undefined,
114
+ modifiedOn: entity.modifiedon ? new Date(entity.modifiedon) : undefined
115
+ });
116
+ }
117
+
118
+ Logger.warn(`Sample Entity not found with ID: ${id}`);
119
+ return null;
120
+ } catch (error) {
121
+ Logger.error(`Error retrieving Sample Entity by ID:`, error);
122
+ throw new Error(`Failed to retrieve Sample Entity: ${error instanceof Error ? error.message : 'Unknown error'}`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Retrieve multiple sampleEntities with optional filter
128
+ */
129
+ public static async retrieveByFilter(apiService: IApiService, filter?: string, top?: number): Promise<SampleEntity[]> {
130
+ try {
131
+ Logger.log(`Retrieving sampleEntities with filter: ${filter || 'none'}`);
132
+
133
+ const topClause = top ? `top="${top}"` : '';
134
+ const filterClause = filter ? `<filter>${filter}</filter>` : '';
135
+
136
+ const fetchXml = `
137
+ <fetch ${topClause}>
138
+ <entity name="${SampleEntityConstants.EntityLogicalName}">
139
+ <attribute name="sample_entityid" />
140
+ <attribute name="name" />
141
+ <attribute name="createdon" />
142
+ <attribute name="modifiedon" />
143
+ <order attribute="name" />
144
+ ${filterClause}
145
+ </entity>
146
+ </fetch>`;
147
+
148
+ const result = await apiService.retrieveMultipleRecords(SampleEntityConstants.EntityLogicalName, fetchXml);
149
+
150
+ const sampleEntities = (result.entities || []).map(entity => new SampleEntity({
151
+ sample_entityid: entity.sample_entityid,
152
+ name: entity.name,
153
+ createdOn: entity.createdon ? new Date(entity.createdon) : undefined,
154
+ modifiedOn: entity.modifiedon ? new Date(entity.modifiedon) : undefined
155
+ }));
156
+
157
+ Logger.log(`Retrieved ${sampleEntities.length} sampleEntities`);
158
+ return sampleEntities;
159
+ } catch (error) {
160
+ Logger.error(`Error retrieving sampleEntities:`, error);
161
+ throw new Error(`Failed to retrieve sampleEntities: ${error instanceof Error ? error.message : 'Unknown error'}`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Update the Sample Entity record
167
+ */
168
+ public async update(apiService: IApiService): Promise<SampleEntity> {
169
+ try {
170
+ if (!this.sample_entityid) {
171
+ throw new Error('Cannot update Sample Entity without ID');
172
+ }
173
+
174
+ Logger.log(`Updating Sample Entity: ${this.name}`);
175
+
176
+ const data = this.toUpdateData();
177
+ await apiService.updateRecord(
178
+ SampleEntityConstants.EntityLogicalName,
179
+ this.sample_entityid,
180
+ data
181
+ );
182
+
183
+ Logger.log(`Sample Entity updated successfully`);
184
+ return this;
185
+ } catch (error) {
186
+ Logger.error(`Error updating Sample Entity:`, error);
187
+ throw new Error(`Failed to update Sample Entity: ${error instanceof Error ? error.message : 'Unknown error'}`);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Delete the Sample Entity record
193
+ */
194
+ public async delete(apiService: IApiService): Promise<void> {
195
+ try {
196
+ if (!this.sample_entityid) {
197
+ throw new Error('Cannot delete Sample Entity without ID');
198
+ }
199
+
200
+ Logger.log(`Deleting Sample Entity: ${this.name}`);
201
+
202
+ await apiService.deleteRecord(
203
+ SampleEntityConstants.EntityLogicalName,
204
+ this.sample_entityid
205
+ );
206
+
207
+ Logger.log(`Sample Entity deleted successfully`);
208
+ } catch (error) {
209
+ Logger.error(`Error deleting Sample Entity:`, error);
210
+ throw new Error(`Failed to delete Sample Entity: ${error instanceof Error ? error.message : 'Unknown error'}`);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Validate the Sample Entity data
216
+ */
217
+ public validate(): { isValid: boolean; errors: string[] } {
218
+ const errors: string[] = [];
219
+
220
+ // Add your validation rules here
221
+ if (!this.name?.trim()) {
222
+ errors.push(`${name} is required`);
223
+ }
224
+
225
+ return {
226
+ isValid: errors.length === 0,
227
+ errors
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Convert to data for create operation
233
+ */
234
+ private toCreateData(): any {
235
+ return {
236
+ name: this.name
237
+ // Add other fields as needed
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Convert to data for update operation
243
+ */
244
+ private toUpdateData(): any {
245
+ return {
246
+ name: this.name
247
+ // Add other fields as needed
248
+ };
249
+ }
250
+ }
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Dynamics 365 Web API Client for Metadata Operations
3
+ * Handles authentication and API calls to retrieve entity metadata
4
+ */
5
+
6
+ const https = require('https');
7
+ const { AuthenticationProvider } = require('@azure/msal-node');
8
+
9
+ class D365MetadataClient {
10
+ constructor(config) {
11
+ this.config = config;
12
+ this.accessToken = null;
13
+ this.tokenExpiry = null;
14
+ }
15
+
16
+ /**
17
+ * Authenticate with Dynamics 365 using service principal
18
+ */
19
+ async authenticate() {
20
+ try {
21
+ const authUrl = `${this.config.authentication.authority}/oauth2/v2.0/token`;
22
+ const authData = new URLSearchParams({
23
+ client_id: this.config.authentication.clientId,
24
+ client_secret: this.config.authentication.clientSecret,
25
+ scope: `${this.config.dynamics365.webApiUrl}/.default`,
26
+ grant_type: 'client_credentials',
27
+ });
28
+
29
+ const response = await this.makeHttpRequest(authUrl, {
30
+ method: 'POST',
31
+ headers: {
32
+ 'Content-Type': 'application/x-www-form-urlencoded',
33
+ },
34
+ body: authData.toString(),
35
+ });
36
+
37
+ this.accessToken = response.access_token;
38
+ this.tokenExpiry = Date.now() + response.expires_in * 1000;
39
+
40
+ return true;
41
+ } catch (error) {
42
+ throw new Error(`Authentication failed: ${error.message}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Ensure we have a valid access token
48
+ */
49
+ async ensureAuthenticated() {
50
+ if (!this.accessToken || Date.now() >= this.tokenExpiry) {
51
+ await this.authenticate();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Get all entity definitions
57
+ */
58
+ async getAllEntities(includeSystemEntities = false) {
59
+ await this.ensureAuthenticated();
60
+
61
+ const filter = includeSystemEntities
62
+ ? ''
63
+ : '?$filter=IsCustomEntity eq true';
64
+ const url = `${this.config.dynamics365.webApiUrl}/EntityDefinitions${filter}&$select=LogicalName,SchemaName,DisplayName,IsCustomEntity`;
65
+
66
+ try {
67
+ const response = await this.makeApiRequest(url);
68
+ return response.value || [];
69
+ } catch (error) {
70
+ throw new Error(`Failed to retrieve entities: ${error.message}`);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get detailed metadata for a specific entity
76
+ */
77
+ async getEntityMetadata(entityLogicalName) {
78
+ await this.ensureAuthenticated();
79
+
80
+ const url =
81
+ `${this.config.dynamics365.webApiUrl}/EntityDefinitions(LogicalName='${entityLogicalName}')` +
82
+ '?$expand=Attributes($select=LogicalName,SchemaName,DisplayName,AttributeType,RequiredLevel,MaxLength,IsPrimaryId,IsValidForCreate,IsValidForUpdate),' +
83
+ 'OneToManyRelationships($select=SchemaName,ReferencingEntity,ReferencingAttribute,ReferencedEntity,ReferencedAttribute),' +
84
+ 'ManyToOneRelationships($select=SchemaName,ReferencingEntity,ReferencingAttribute,ReferencedEntity,ReferencedAttribute)';
85
+
86
+ try {
87
+ const response = await this.makeApiRequest(url);
88
+ return this.transformEntityMetadata(response);
89
+ } catch (error) {
90
+ throw new Error(
91
+ `Failed to retrieve metadata for ${entityLogicalName}: ${error.message}`
92
+ );
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get option set metadata
98
+ */
99
+ async getOptionSetMetadata(optionSetName) {
100
+ await this.ensureAuthenticated();
101
+
102
+ const url =
103
+ `${this.config.dynamics365.webApiUrl}/GlobalOptionSetDefinitions(Name='${optionSetName}')` +
104
+ '?$expand=Options($select=Value,Label)';
105
+
106
+ try {
107
+ const response = await this.makeApiRequest(url);
108
+ return this.transformOptionSetMetadata(response);
109
+ } catch (error) {
110
+ throw new Error(
111
+ `Failed to retrieve option set ${optionSetName}: ${error.message}`
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get all custom entities with their basic metadata
118
+ */
119
+ async getCustomEntities() {
120
+ await this.ensureAuthenticated();
121
+
122
+ const url =
123
+ `${this.config.dynamics365.webApiUrl}/EntityDefinitions` +
124
+ '?$filter=IsCustomEntity eq true&$select=LogicalName,SchemaName,DisplayName,PrimaryIdAttribute,PrimaryNameAttribute,EntitySetName';
125
+
126
+ try {
127
+ const response = await this.makeApiRequest(url);
128
+ return response.value || [];
129
+ } catch (error) {
130
+ throw new Error(`Failed to retrieve custom entities: ${error.message}`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Validate entity exists and get basic info
136
+ */
137
+ async validateEntity(entityLogicalName) {
138
+ await this.ensureAuthenticated();
139
+
140
+ const url =
141
+ `${this.config.dynamics365.webApiUrl}/EntityDefinitions(LogicalName='${entityLogicalName}')` +
142
+ '?$select=LogicalName,SchemaName,DisplayName,IsCustomEntity,PrimaryIdAttribute,PrimaryNameAttribute';
143
+
144
+ try {
145
+ const response = await this.makeApiRequest(url);
146
+ return {
147
+ exists: true,
148
+ metadata: response,
149
+ };
150
+ } catch (error) {
151
+ if (error.status === 404) {
152
+ return {
153
+ exists: false,
154
+ error: `Entity '${entityLogicalName}' not found`,
155
+ };
156
+ }
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Transform D365 metadata to our standardized format
163
+ */
164
+ transformEntityMetadata(d365Metadata) {
165
+ return {
166
+ LogicalName: d365Metadata.LogicalName,
167
+ SchemaName: d365Metadata.SchemaName,
168
+ DisplayName: this.getLocalizedLabel(d365Metadata.DisplayName),
169
+ PrimaryIdAttribute: d365Metadata.PrimaryIdAttribute,
170
+ PrimaryNameAttribute: d365Metadata.PrimaryNameAttribute,
171
+ CollectionSchemaName: d365Metadata.CollectionSchemaName,
172
+ EntitySetName: d365Metadata.EntitySetName,
173
+ IsCustomEntity: d365Metadata.IsCustomEntity,
174
+ Attributes: this.transformAttributes(d365Metadata.Attributes || []),
175
+ OneToManyRelationships: this.transformRelationships(
176
+ d365Metadata.OneToManyRelationships || []
177
+ ),
178
+ ManyToOneRelationships: this.transformRelationships(
179
+ d365Metadata.ManyToOneRelationships || []
180
+ ),
181
+ LastModified: new Date().toISOString(),
182
+ Metadata: {
183
+ SyncedAt: new Date().toISOString(),
184
+ SyncedFrom: this.config.dynamics365.webApiUrl,
185
+ Version: '1.0',
186
+ },
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Transform D365 attributes to our format
192
+ */
193
+ transformAttributes(d365Attributes) {
194
+ return d365Attributes.map((attr) => ({
195
+ LogicalName: attr.LogicalName,
196
+ SchemaName: attr.SchemaName,
197
+ DisplayName: this.getLocalizedLabel(attr.DisplayName),
198
+ AttributeType: this.mapAttributeType(attr.AttributeType),
199
+ RequiredLevel: this.mapRequiredLevel(attr.RequiredLevel),
200
+ MaxLength: attr.MaxLength,
201
+ IsPrimaryId: attr.IsPrimaryId,
202
+ IsValidForCreate: attr.IsValidForCreate,
203
+ IsValidForUpdate: attr.IsValidForUpdate,
204
+ IsLogical: attr.IsLogical,
205
+ OptionSet: attr.OptionSet?.Name,
206
+ Target: attr.Targets ? attr.Targets[0] : undefined, // For lookup fields
207
+ Precision: attr.Precision,
208
+ MinValue: attr.MinValue,
209
+ MaxValue: attr.MaxValue,
210
+ }));
211
+ }
212
+
213
+ /**
214
+ * Transform D365 relationships to our format
215
+ */
216
+ transformRelationships(d365Relationships) {
217
+ return d365Relationships.map((rel) => ({
218
+ SchemaName: rel.SchemaName,
219
+ ReferencingEntity: rel.ReferencingEntity,
220
+ ReferencingAttribute: rel.ReferencingAttribute,
221
+ ReferencedEntity: rel.ReferencedEntity,
222
+ ReferencedAttribute: rel.ReferencedAttribute,
223
+ RelationshipType: rel.RelationshipType,
224
+ }));
225
+ }
226
+
227
+ /**
228
+ * Transform option set metadata
229
+ */
230
+ transformOptionSetMetadata(d365OptionSet) {
231
+ return {
232
+ Name: d365OptionSet.Name,
233
+ DisplayName: this.getLocalizedLabel(d365OptionSet.DisplayName),
234
+ Description: this.getLocalizedLabel(d365OptionSet.Description),
235
+ Options: (d365OptionSet.Options || []).map((option) => ({
236
+ Value: option.Value,
237
+ Label: this.getLocalizedLabel(option.Label),
238
+ Description: this.getLocalizedLabel(option.Description),
239
+ })),
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Map D365 attribute types to our standardized types
245
+ */
246
+ mapAttributeType(d365Type) {
247
+ const typeMap = {
248
+ String: 'String',
249
+ Memo: 'Memo',
250
+ Integer: 'Integer',
251
+ BigInt: 'BigInt',
252
+ Double: 'Double',
253
+ Decimal: 'Decimal',
254
+ Money: 'Money',
255
+ Boolean: 'Boolean',
256
+ DateTime: 'DateTime',
257
+ Uniqueidentifier: 'Uniqueidentifier',
258
+ Picklist: 'Picklist',
259
+ State: 'State',
260
+ Status: 'Status',
261
+ Lookup: 'Lookup',
262
+ Customer: 'Customer',
263
+ Owner: 'Owner',
264
+ MultiSelectPicklist: 'MultiSelectPicklist',
265
+ Virtual: 'Virtual',
266
+ EntityName: 'EntityName',
267
+ ManagedProperty: 'ManagedProperty',
268
+ };
269
+
270
+ return typeMap[d365Type] || d365Type;
271
+ }
272
+
273
+ /**
274
+ * Map D365 required levels to our format
275
+ */
276
+ mapRequiredLevel(d365RequiredLevel) {
277
+ const levelMap = {
278
+ ApplicationRequired: 'ApplicationRequired',
279
+ SystemRequired: 'SystemRequired',
280
+ Recommended: 'Recommended',
281
+ None: 'None',
282
+ };
283
+
284
+ return levelMap[d365RequiredLevel] || d365RequiredLevel;
285
+ }
286
+
287
+ /**
288
+ * Extract localized label (defaults to English)
289
+ */
290
+ getLocalizedLabel(labelCollection) {
291
+ if (!labelCollection || !labelCollection.LocalizedLabels) {
292
+ return '';
293
+ }
294
+
295
+ // Try to find English label first
296
+ const englishLabel = labelCollection.LocalizedLabels.find(
297
+ (label) => label.LanguageCode === 1033
298
+ );
299
+
300
+ if (englishLabel) {
301
+ return englishLabel.Label;
302
+ }
303
+
304
+ // Fall back to first available label
305
+ if (labelCollection.LocalizedLabels.length > 0) {
306
+ return labelCollection.LocalizedLabels[0].Label;
307
+ }
308
+
309
+ return '';
310
+ }
311
+
312
+ /**
313
+ * Make authenticated API request to D365
314
+ */
315
+ async makeApiRequest(url, options = {}) {
316
+ const headers = {
317
+ Authorization: `Bearer ${this.accessToken}`,
318
+ Accept: 'application/json',
319
+ 'Content-Type': 'application/json',
320
+ 'OData-MaxVersion': '4.0',
321
+ 'OData-Version': '4.0',
322
+ ...options.headers,
323
+ };
324
+
325
+ return this.makeHttpRequest(url, {
326
+ ...options,
327
+ headers,
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Make HTTP request with proper error handling
333
+ */
334
+ async makeHttpRequest(url, options = {}) {
335
+ return new Promise((resolve, reject) => {
336
+ const urlObj = new URL(url);
337
+ const requestOptions = {
338
+ hostname: urlObj.hostname,
339
+ port: urlObj.port || 443,
340
+ path: urlObj.pathname + urlObj.search,
341
+ method: options.method || 'GET',
342
+ headers: options.headers || {},
343
+ };
344
+
345
+ const req = https.request(requestOptions, (res) => {
346
+ let data = '';
347
+
348
+ res.on('data', (chunk) => {
349
+ data += chunk;
350
+ });
351
+
352
+ res.on('end', () => {
353
+ try {
354
+ if (res.statusCode >= 200 && res.statusCode < 300) {
355
+ const jsonData = data ? JSON.parse(data) : {};
356
+ resolve(jsonData);
357
+ } else {
358
+ const error = new Error(
359
+ `HTTP ${res.statusCode}: ${res.statusMessage}`
360
+ );
361
+ error.status = res.statusCode;
362
+ error.response = data;
363
+ reject(error);
364
+ }
365
+ } catch (parseError) {
366
+ reject(
367
+ new Error(`Failed to parse response: ${parseError.message}`)
368
+ );
369
+ }
370
+ });
371
+ });
372
+
373
+ req.on('error', (error) => {
374
+ reject(new Error(`Request failed: ${error.message}`));
375
+ });
376
+
377
+ if (options.body) {
378
+ req.write(options.body);
379
+ }
380
+
381
+ req.end();
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Test connection to D365
387
+ */
388
+ async testConnection() {
389
+ try {
390
+ await this.ensureAuthenticated();
391
+
392
+ // Make a simple request to validate connection
393
+ const url = `${this.config.dynamics365.webApiUrl}/WhoAmI()`;
394
+ const response = await this.makeApiRequest(url);
395
+
396
+ return {
397
+ success: true,
398
+ userId: response.UserId,
399
+ organizationId: response.OrganizationId,
400
+ };
401
+ } catch (error) {
402
+ return {
403
+ success: false,
404
+ error: error.message,
405
+ };
406
+ }
407
+ }
408
+ }
409
+
410
+ module.exports = { D365MetadataClient };