@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.
- package/bin/create-dynamics-app.js +1 -1
- package/dist/index.js +140 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/consultingHelpers.d.ts +13 -0
- package/dist/utils/consultingHelpers.d.ts.map +1 -0
- package/dist/utils/consultingHelpers.js +569 -0
- package/dist/utils/consultingHelpers.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/initGit.d.ts.map +1 -1
- package/dist/utils/initGit.js.map +1 -1
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +3 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +1 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +11 -1
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +1 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
- package/templates/dynamics-365-starter/README.md +566 -137
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
- package/templates/dynamics-365-starter/deployment/README.md +484 -0
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
- package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
- package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
- package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
- package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
- package/templates/dynamics-365-starter/package.json +22 -1
- package/templates/dynamics-365-starter/public/index.html +8 -11
- package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
- package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
- package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
- package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
- package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
- package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
- package/templates/dynamics-365-starter/src/examples/README.md +52 -0
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
- package/templates/dynamics-365-starter/src/index.tsx +107 -19
- package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
- package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
- package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
- package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
- package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
- package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
- package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
- package/templates/dynamics-365-starter/src/styles/index.css +74 -7
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
- package/templates/dynamics-365-starter/tsconfig.json +11 -8
- package/templates/dynamics-365-starter/webpack.config.js +8 -9
- package/templates/power-pages-starter/README.md +7 -1
- package/templates/power-pages-starter/public/index.html +8 -11
- package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
- package/templates/power-pages-starter/src/index.tsx +3 -3
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
- package/templates/power-pages-starter/tsconfig.json +3 -9
- 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 };
|