@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
@@ -1,114 +1,329 @@
1
1
  import React, { createContext, useContext, useState, useEffect } from 'react';
2
- import { DynamicsApiService } from '@khester/dynamics-ui-api-client';
2
+ import { IApiService } from '@khester/dynamics-ui-api-client';
3
+ import { ServiceFactory } from '../services/ServiceFactory';
4
+ import { Logger } from '../components/Logging/logger';
3
5
 
4
6
  interface DynamicsContextType {
5
- apiService: DynamicsApiService | null;
7
+ apiService: IApiService | null;
6
8
  createRecord: (entityName: string, data: any) => Promise<any>;
7
- retrieveRecord: (entityName: string, id: string, select?: string) => Promise<any>;
9
+ retrieveRecord: (
10
+ entityName: string,
11
+ id: string,
12
+ select?: string
13
+ ) => Promise<any>;
8
14
  updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
9
15
  deleteRecord: (entityName: string, id: string) => Promise<void>;
10
16
  retrieveMultiple: (entityName: string, query?: string) => Promise<any>;
17
+ isEnvironmentMock: boolean;
18
+ environmentType: 'mock' | 'production';
11
19
  }
12
20
 
13
- const DynamicsContext = createContext<DynamicsContextType | undefined>(undefined);
21
+ const DynamicsContext = createContext<DynamicsContextType | undefined>(
22
+ undefined
23
+ );
14
24
 
15
25
  interface DynamicsProviderProps {
16
26
  children: React.ReactNode;
17
- baseUrl?: string;
18
- accessToken?: string;
27
+ /** Optional Xrm object for Dynamics 365 environment (will be auto-detected if not provided) */
28
+ xrm?: any;
29
+ /** Optional custom API service for PCF or other integrations */
30
+ customApiService?: IApiService;
19
31
  }
20
32
 
21
- export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
22
- children,
23
- baseUrl,
24
- accessToken
33
+ export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
34
+ children,
35
+ xrm,
36
+ customApiService,
25
37
  }) => {
26
- const [apiService, setApiService] = useState<DynamicsApiService | null>(null);
38
+ const [apiService, setApiService] = useState<IApiService | null>(null);
39
+ const [environmentType, setEnvironmentType] = useState<'mock' | 'production'>(
40
+ 'mock'
41
+ );
27
42
 
28
43
  useEffect(() => {
29
- // Initialize the API service
30
- // In a real D365 environment, you would get these from your authentication flow
31
- const getAccessToken = async () => {
32
- return accessToken || process.env.DYNAMICS_ACCESS_TOKEN || '';
33
- };
34
-
35
- const service = new DynamicsApiService(
36
- baseUrl || process.env.DYNAMICS_BASE_URL || 'https://org.crm.dynamics.com',
37
- getAccessToken
38
- );
39
-
40
- setApiService(service);
41
- }, [baseUrl, accessToken]);
44
+ try {
45
+ Logger.log(
46
+ 'DynamicsProvider: Initializing API service',
47
+ 'DynamicsProvider'
48
+ );
49
+
50
+ // Use custom API service if provided (for PCF integration)
51
+ if (customApiService) {
52
+ Logger.log(
53
+ 'Using custom API service provided via props',
54
+ 'DynamicsProvider'
55
+ );
56
+ setApiService(customApiService);
57
+ setEnvironmentType('production'); // Assume production for custom services
58
+ return;
59
+ }
60
+
61
+ // Detect Xrm object if not provided
62
+ const xrmObject =
63
+ xrm || (typeof window !== 'undefined' && (window as any).Xrm);
64
+
65
+ // Use ServiceFactory to create the appropriate service
66
+ const service = ServiceFactory.createApiService(xrmObject);
67
+ const envType = ServiceFactory.getEnvironmentType();
68
+
69
+ setApiService(service);
70
+ setEnvironmentType(envType);
71
+
72
+ Logger.log(
73
+ `DynamicsProvider: Initialized with ${envType} environment`,
74
+ 'DynamicsProvider'
75
+ );
76
+
77
+ if (envType === 'production' && !ServiceFactory.isDynamics365Context()) {
78
+ Logger.warn(
79
+ 'Running in production mode but Xrm object may not be available',
80
+ 'DynamicsProvider'
81
+ );
82
+ }
83
+ } catch (error) {
84
+ Logger.error(
85
+ 'Failed to initialize API service',
86
+ 'DynamicsProvider',
87
+ error
88
+ );
89
+
90
+ // Fallback to mock service for development
91
+ try {
92
+ const mockService = ServiceFactory.createApiService();
93
+ setApiService(mockService);
94
+ setEnvironmentType('mock');
95
+ Logger.warn(
96
+ 'Falling back to mock service due to initialization error',
97
+ 'DynamicsProvider'
98
+ );
99
+ } catch (fallbackError) {
100
+ Logger.error(
101
+ 'Failed to initialize fallback mock service',
102
+ 'DynamicsProvider',
103
+ fallbackError
104
+ );
105
+ }
106
+ }
107
+ }, [xrm, customApiService]);
42
108
 
43
109
  const createRecord = async (entityName: string, data: any) => {
44
- if (!apiService) throw new Error('API service not initialized');
45
- return await apiService.createRecord(entityName, data);
110
+ if (!apiService) {
111
+ const error = 'API service not initialized';
112
+ Logger.error(error, 'DynamicsProvider.createRecord');
113
+ throw new Error(error);
114
+ }
115
+
116
+ try {
117
+ Logger.apiOperation(
118
+ 'CREATE',
119
+ entityName,
120
+ data,
121
+ 'DynamicsProvider.createRecord'
122
+ );
123
+ const result = await apiService.createRecord(entityName, data);
124
+ Logger.log(
125
+ `Successfully created ${entityName} record`,
126
+ 'DynamicsProvider.createRecord'
127
+ );
128
+ return result;
129
+ } catch (error) {
130
+ Logger.error(
131
+ `Failed to create ${entityName} record`,
132
+ 'DynamicsProvider.createRecord',
133
+ error
134
+ );
135
+ throw error;
136
+ }
46
137
  };
47
138
 
48
- const retrieveRecord = async (entityName: string, id: string, select?: string) => {
49
- if (!apiService) throw new Error('API service not initialized');
50
- // Use retrieveMultipleRecords with FetchXML to get a single record
51
- const selectAttributes = select ? select.split(',').map(attr => attr.trim()) : ['*'];
52
- const attributes = selectAttributes.map(attr => attr === '*' ? '' : `<attribute name="${attr}" />`).join('');
53
- const fetchXml = `
54
- <fetch top="1">
55
- <entity name="${entityName}">
56
- ${attributes}
57
- <filter>
58
- <condition attribute="${entityName}id" operator="eq" value="${id}" />
59
- </filter>
60
- </entity>
61
- </fetch>
62
- `;
63
- const result = await apiService.retrieveMultipleRecords(entityName, fetchXml);
64
- return result.entities.length > 0 ? result.entities[0] : null;
139
+ const retrieveRecord = async (
140
+ entityName: string,
141
+ id: string,
142
+ select?: string
143
+ ) => {
144
+ if (!apiService) {
145
+ const error = 'API service not initialized';
146
+ Logger.error(error, 'DynamicsProvider.retrieveRecord');
147
+ throw new Error(error);
148
+ }
149
+
150
+ try {
151
+ Logger.apiOperation(
152
+ 'READ',
153
+ entityName,
154
+ { id, select },
155
+ 'DynamicsProvider.retrieveRecord'
156
+ );
157
+
158
+ // Use retrieveMultipleRecords with FetchXML to get a single record
159
+ const selectAttributes = select
160
+ ? select.split(',').map((attr) => attr.trim())
161
+ : ['*'];
162
+ const attributes = selectAttributes
163
+ .map((attr) => (attr === '*' ? '' : `<attribute name="${attr}" />`))
164
+ .join('');
165
+ const fetchXml = `
166
+ <fetch top="1">
167
+ <entity name="${entityName}">
168
+ ${attributes}
169
+ <filter>
170
+ <condition attribute="${entityName}id" operator="eq" value="${id}" />
171
+ </filter>
172
+ </entity>
173
+ </fetch>
174
+ `;
175
+
176
+ Logger.fetchXml(fetchXml, 'DynamicsProvider.retrieveRecord');
177
+ const result = await apiService.retrieveMultipleRecords(
178
+ entityName,
179
+ fetchXml
180
+ );
181
+ const record = result.entities.length > 0 ? result.entities[0] : null;
182
+
183
+ Logger.log(
184
+ `Retrieved ${entityName} record: ${record ? 'found' : 'not found'}`,
185
+ 'DynamicsProvider.retrieveRecord'
186
+ );
187
+
188
+ return record;
189
+ } catch (error) {
190
+ Logger.error(
191
+ `Failed to retrieve ${entityName} record`,
192
+ 'DynamicsProvider.retrieveRecord',
193
+ error
194
+ );
195
+ throw error;
196
+ }
65
197
  };
66
198
 
67
199
  const updateRecord = async (entityName: string, id: string, data: any) => {
68
- if (!apiService) throw new Error('API service not initialized');
69
- return await apiService.updateRecord(entityName, id, data);
200
+ if (!apiService) {
201
+ const error = 'API service not initialized';
202
+ Logger.error(error, 'DynamicsProvider.updateRecord');
203
+ throw new Error(error);
204
+ }
205
+
206
+ try {
207
+ Logger.apiOperation(
208
+ 'UPDATE',
209
+ entityName,
210
+ { id, data },
211
+ 'DynamicsProvider.updateRecord'
212
+ );
213
+ const result = await apiService.updateRecord(entityName, id, data);
214
+ Logger.log(
215
+ `Successfully updated ${entityName} record`,
216
+ 'DynamicsProvider.updateRecord'
217
+ );
218
+ return result;
219
+ } catch (error) {
220
+ Logger.error(
221
+ `Failed to update ${entityName} record`,
222
+ 'DynamicsProvider.updateRecord',
223
+ error
224
+ );
225
+ throw error;
226
+ }
70
227
  };
71
228
 
72
229
  const deleteRecord = async (entityName: string, id: string) => {
73
- if (!apiService) throw new Error('API service not initialized');
74
- return await apiService.deleteRecord(entityName, id);
230
+ if (!apiService) {
231
+ const error = 'API service not initialized';
232
+ Logger.error(error, 'DynamicsProvider.deleteRecord');
233
+ throw new Error(error);
234
+ }
235
+
236
+ try {
237
+ Logger.apiOperation(
238
+ 'DELETE',
239
+ entityName,
240
+ { id },
241
+ 'DynamicsProvider.deleteRecord'
242
+ );
243
+ await apiService.deleteRecord(entityName, id);
244
+ Logger.log(
245
+ `Successfully deleted ${entityName} record`,
246
+ 'DynamicsProvider.deleteRecord'
247
+ );
248
+ } catch (error) {
249
+ Logger.error(
250
+ `Failed to delete ${entityName} record`,
251
+ 'DynamicsProvider.deleteRecord',
252
+ error
253
+ );
254
+ throw error;
255
+ }
75
256
  };
76
257
 
77
258
  const retrieveMultiple = async (entityName: string, query?: string) => {
78
- if (!apiService) throw new Error('API service not initialized');
79
- // Convert OData-style query to FetchXML
80
- let fetchXml = `<fetch>`;
81
-
82
- if (query) {
83
- // Parse basic OData query parameters
84
- const selectMatch = query.match(/\$select=([^&]*)/i);
85
- const orderByMatch = query.match(/\$orderby=([^&]*)/i);
86
- const topMatch = query.match(/\$top=(\d+)/i);
87
-
88
- if (topMatch) {
89
- fetchXml = `<fetch top="${topMatch[1]}">`;
90
- }
91
-
92
- fetchXml += `<entity name="${entityName}">`;
93
-
94
- if (selectMatch) {
95
- const attributes = selectMatch[1].split(',').map(attr => attr.trim());
96
- attributes.forEach(attr => {
97
- fetchXml += `<attribute name="${attr}" />`;
98
- });
99
- }
100
-
101
- if (orderByMatch) {
102
- const [field, direction] = orderByMatch[1].split(' ');
103
- fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
259
+ if (!apiService) {
260
+ const error = 'API service not initialized';
261
+ Logger.error(error, 'DynamicsProvider.retrieveMultiple');
262
+ throw new Error(error);
263
+ }
264
+
265
+ try {
266
+ Logger.apiOperation(
267
+ 'QUERY',
268
+ entityName,
269
+ query,
270
+ 'DynamicsProvider.retrieveMultiple'
271
+ );
272
+
273
+ // Convert OData-style query to FetchXML
274
+ let fetchXml = `<fetch>`;
275
+
276
+ if (query) {
277
+ // Parse basic OData query parameters
278
+ const selectMatch = query.match(/\$select=([^&]*)/i);
279
+ const orderByMatch = query.match(/\$orderby=([^&]*)/i);
280
+ const topMatch = query.match(/\$top=(\d+)/i);
281
+
282
+ if (topMatch) {
283
+ fetchXml = `<fetch top="${topMatch[1]}">`;
284
+ }
285
+
286
+ fetchXml += `<entity name="${entityName}">`;
287
+
288
+ if (selectMatch) {
289
+ const attributes = selectMatch[1]
290
+ .split(',')
291
+ .map((attr) => attr.trim());
292
+ attributes.forEach((attr) => {
293
+ fetchXml += `<attribute name="${attr}" />`;
294
+ });
295
+ }
296
+
297
+ if (orderByMatch) {
298
+ const [field, direction] = orderByMatch[1].split(' ');
299
+ fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
300
+ }
301
+
302
+ fetchXml += `</entity></fetch>`;
303
+ } else {
304
+ fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
104
305
  }
105
-
106
- fetchXml += `</entity></fetch>`;
107
- } else {
108
- fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
306
+
307
+ Logger.fetchXml(fetchXml, 'DynamicsProvider.retrieveMultiple');
308
+ const result = await apiService.retrieveMultipleRecords(
309
+ entityName,
310
+ fetchXml
311
+ );
312
+
313
+ Logger.log(
314
+ `Retrieved ${result.entities.length} ${entityName} records`,
315
+ 'DynamicsProvider.retrieveMultiple'
316
+ );
317
+
318
+ return result;
319
+ } catch (error) {
320
+ Logger.error(
321
+ `Failed to retrieve ${entityName} records`,
322
+ 'DynamicsProvider.retrieveMultiple',
323
+ error
324
+ );
325
+ throw error;
109
326
  }
110
-
111
- return await apiService.retrieveMultipleRecords(entityName, fetchXml);
112
327
  };
113
328
 
114
329
  const value: DynamicsContextType = {
@@ -118,6 +333,8 @@ export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
118
333
  updateRecord,
119
334
  deleteRecord,
120
335
  retrieveMultiple,
336
+ isEnvironmentMock: environmentType === 'mock',
337
+ environmentType,
121
338
  };
122
339
 
123
340
  return (
@@ -133,4 +350,4 @@ export const useDynamicsApi = (): DynamicsContextType => {
133
350
  throw new Error('useDynamicsApi must be used within a DynamicsProvider');
134
351
  }
135
352
  return context;
136
- };
353
+ };
@@ -0,0 +1,260 @@
1
+ import { IApiService } from '@khester/dynamics-ui-api-client';
2
+
3
+ /**
4
+ * Mock implementation of IApiService for local development and testing.
5
+ * Provides in-memory storage and simulated API responses.
6
+ */
7
+ export class MockApiService implements IApiService {
8
+ private storage: Map<string, Map<string, any>> = new Map();
9
+
10
+ constructor() {
11
+ // Initialize with some mock data
12
+ this.initializeMockData();
13
+ }
14
+
15
+ async createRecord(entityName: string, record: any): Promise<any> {
16
+ console.log(`MockApiService: Creating ${entityName} record`, record);
17
+
18
+ // Simulate network delay
19
+ await this.simulateDelay();
20
+
21
+ // Get or create entity collection
22
+ if (!this.storage.has(entityName)) {
23
+ this.storage.set(entityName, new Map());
24
+ }
25
+ const collection = this.storage.get(entityName)!;
26
+
27
+ // Generate ID based on entity type
28
+ const id = this.generateId(entityName);
29
+ const primaryKey = this.getPrimaryKey(entityName);
30
+
31
+ // Create record with ID
32
+ const newRecord = {
33
+ ...record,
34
+ [primaryKey]: id,
35
+ createdon: new Date().toISOString(),
36
+ modifiedon: new Date().toISOString(),
37
+ };
38
+
39
+ collection.set(id, newRecord);
40
+
41
+ return newRecord;
42
+ }
43
+
44
+ async updateRecord(
45
+ entityName: string,
46
+ id: string,
47
+ record: any
48
+ ): Promise<any> {
49
+ console.log(`MockApiService: Updating ${entityName} record ${id}`, record);
50
+
51
+ await this.simulateDelay();
52
+
53
+ const collection = this.storage.get(entityName);
54
+ if (!collection || !collection.has(id)) {
55
+ throw new Error(`Record not found: ${entityName} with ID ${id}`);
56
+ }
57
+
58
+ const existing = collection.get(id);
59
+ const updated = {
60
+ ...existing,
61
+ ...record,
62
+ modifiedon: new Date().toISOString(),
63
+ };
64
+
65
+ collection.set(id, updated);
66
+
67
+ return updated;
68
+ }
69
+
70
+ async deleteRecord(entityName: string, id: string): Promise<void> {
71
+ console.log(`MockApiService: Deleting ${entityName} record ${id}`);
72
+
73
+ await this.simulateDelay();
74
+
75
+ const collection = this.storage.get(entityName);
76
+ if (!collection || !collection.has(id)) {
77
+ throw new Error(`Record not found: ${entityName} with ID ${id}`);
78
+ }
79
+
80
+ collection.delete(id);
81
+ }
82
+
83
+ async retrieveMultipleRecords(
84
+ entityName: string,
85
+ fetchXml: string
86
+ ): Promise<{ entities: any[] }> {
87
+ console.log(
88
+ `MockApiService: Retrieving ${entityName} records with FetchXML`,
89
+ fetchXml
90
+ );
91
+
92
+ await this.simulateDelay();
93
+
94
+ const collection = this.storage.get(entityName);
95
+ if (!collection) {
96
+ return { entities: [] };
97
+ }
98
+
99
+ // Convert Map values to array
100
+ const entities = Array.from(collection.values());
101
+
102
+ // Simple filter parsing from FetchXML (basic implementation)
103
+ const filteredEntities = this.applyBasicFetchXmlFilter(entities, fetchXml);
104
+
105
+ return { entities: filteredEntities };
106
+ }
107
+
108
+ async executeRequest(requestName: string, requestData: any): Promise<any> {
109
+ console.log(
110
+ `MockApiService: Executing request ${requestName}`,
111
+ requestData
112
+ );
113
+
114
+ await this.simulateDelay();
115
+
116
+ // Mock implementation - return success response
117
+ return {
118
+ success: true,
119
+ requestName,
120
+ timestamp: new Date().toISOString(),
121
+ data: requestData,
122
+ };
123
+ }
124
+
125
+ async uploadFile(file: File): Promise<string> {
126
+ console.log(`MockApiService: Uploading file ${file.name}`);
127
+
128
+ await this.simulateDelay(1000); // Longer delay for file upload
129
+
130
+ // Return mock file URL
131
+ return `https://mock-storage.dynamics365.com/files/${this.generateId('file')}-${file.name}`;
132
+ }
133
+
134
+ /**
135
+ * Initialize mock data for development
136
+ */
137
+ private initializeMockData(): void {
138
+ // Create accounts collection
139
+ const accounts = new Map<string, any>();
140
+ accounts.set('00000000-0000-0000-0000-000000000001', {
141
+ accountid: '00000000-0000-0000-0000-000000000001',
142
+ name: 'Contoso Ltd',
143
+ emailaddress1: 'info@contoso.com',
144
+ telephone1: '555-0100',
145
+ address1_city: 'Seattle',
146
+ address1_stateorprovince: 'WA',
147
+ address1_country: 'USA',
148
+ revenue: 5000000,
149
+ numberofemployees: 250,
150
+ createdon: '2024-01-15T10:00:00Z',
151
+ modifiedon: '2024-01-15T10:00:00Z',
152
+ });
153
+ accounts.set('00000000-0000-0000-0000-000000000002', {
154
+ accountid: '00000000-0000-0000-0000-000000000002',
155
+ name: 'Adventure Works',
156
+ emailaddress1: 'contact@adventureworks.com',
157
+ telephone1: '555-0200',
158
+ address1_city: 'Redmond',
159
+ address1_stateorprovince: 'WA',
160
+ address1_country: 'USA',
161
+ revenue: 10000000,
162
+ numberofemployees: 500,
163
+ createdon: '2024-01-20T14:30:00Z',
164
+ modifiedon: '2024-01-20T14:30:00Z',
165
+ });
166
+ this.storage.set('accounts', accounts);
167
+
168
+ // Create contacts collection
169
+ const contacts = new Map<string, any>();
170
+ contacts.set('00000000-0000-0000-0000-000000000101', {
171
+ contactid: '00000000-0000-0000-0000-000000000101',
172
+ firstname: 'John',
173
+ lastname: 'Doe',
174
+ emailaddress1: 'john.doe@contoso.com',
175
+ telephone1: '555-0101',
176
+ parentcustomerid: '00000000-0000-0000-0000-000000000001',
177
+ parentcustomerid_account: {
178
+ accountid: '00000000-0000-0000-0000-000000000001',
179
+ name: 'Contoso Ltd',
180
+ },
181
+ createdon: '2024-01-16T09:00:00Z',
182
+ modifiedon: '2024-01-16T09:00:00Z',
183
+ });
184
+ contacts.set('00000000-0000-0000-0000-000000000102', {
185
+ contactid: '00000000-0000-0000-0000-000000000102',
186
+ firstname: 'Jane',
187
+ lastname: 'Smith',
188
+ emailaddress1: 'jane.smith@adventureworks.com',
189
+ telephone1: '555-0201',
190
+ parentcustomerid: '00000000-0000-0000-0000-000000000002',
191
+ parentcustomerid_account: {
192
+ accountid: '00000000-0000-0000-0000-000000000002',
193
+ name: 'Adventure Works',
194
+ },
195
+ createdon: '2024-01-21T11:00:00Z',
196
+ modifiedon: '2024-01-21T11:00:00Z',
197
+ });
198
+ this.storage.set('contacts', contacts);
199
+ }
200
+
201
+ /**
202
+ * Generate a mock ID for an entity
203
+ */
204
+ private generateId(entityName: string): string {
205
+ const timestamp = Date.now();
206
+ const random = Math.floor(Math.random() * 1000000);
207
+ return `${entityName}-${timestamp}-${random}`;
208
+ }
209
+
210
+ /**
211
+ * Get the primary key field name for an entity
212
+ */
213
+ private getPrimaryKey(entityName: string): string {
214
+ const primaryKeys: Record<string, string> = {
215
+ accounts: 'accountid',
216
+ contacts: 'contactid',
217
+ opportunities: 'opportunityid',
218
+ leads: 'leadid',
219
+ incidents: 'incidentid',
220
+ quotes: 'quoteid',
221
+ salesorders: 'salesorderid',
222
+ invoices: 'invoiceid',
223
+ };
224
+
225
+ return primaryKeys[entityName] || `${entityName.replace(/s$/, '')}id`;
226
+ }
227
+
228
+ /**
229
+ * Apply basic FetchXML filtering (simplified implementation)
230
+ */
231
+ private applyBasicFetchXmlFilter(entities: any[], fetchXml: string): any[] {
232
+ // Extract attribute names from FetchXML
233
+ const attributeMatches = fetchXml.match(/<attribute name="([^"]+)"/g);
234
+ if (!attributeMatches || attributeMatches.length === 0) {
235
+ return entities;
236
+ }
237
+
238
+ const attributes = attributeMatches.map((match) =>
239
+ match.replace('<attribute name="', '').replace('"', '')
240
+ );
241
+
242
+ // Filter entities to only include requested attributes
243
+ return entities.map((entity) => {
244
+ const filtered: any = {};
245
+ attributes.forEach((attr) => {
246
+ if (Object.prototype.hasOwnProperty.call(entity, attr)) {
247
+ filtered[attr] = entity[attr];
248
+ }
249
+ });
250
+ return filtered;
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Simulate network delay for realistic behavior
256
+ */
257
+ private async simulateDelay(ms: number = 200): Promise<void> {
258
+ return new Promise((resolve) => setTimeout(resolve, ms));
259
+ }
260
+ }