@khester/create-dynamics-app 2.6.2 → 2.6.4

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 (47) hide show
  1. package/dist/artifacts/registry.d.ts.map +1 -1
  2. package/dist/artifacts/registry.js +70 -0
  3. package/dist/artifacts/registry.js.map +1 -1
  4. package/dist/artifacts/types.d.ts +17 -1
  5. package/dist/artifacts/types.d.ts.map +1 -1
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/injectSharedModules.d.ts +21 -0
  9. package/dist/injectSharedModules.d.ts.map +1 -0
  10. package/dist/injectSharedModules.js +84 -0
  11. package/dist/injectSharedModules.js.map +1 -0
  12. package/dist/scaffold.d.ts +1 -0
  13. package/dist/scaffold.d.ts.map +1 -1
  14. package/dist/scaffold.js +2 -0
  15. package/dist/scaffold.js.map +1 -1
  16. package/package.json +1 -1
  17. package/templates/{grid-starter/src/core/services → _shared/dataverse-seam}/FetchApiService.ts +3 -8
  18. package/templates/{grid-starter/src/core/services → _shared/dataverse-seam}/IApiService.ts +16 -8
  19. package/templates/{react-custom-page/src/core/services → _shared/dataverse-seam}/XrmApiService.ts +6 -17
  20. package/templates/grid-starter/src/core/services/MockApiService.ts +0 -4
  21. package/templates/host-swap/README.md +35 -0
  22. package/templates/host-swap/_variants/host.custom-page/env.example +5 -0
  23. package/templates/host-swap/_variants/host.custom-page/gitignore +5 -0
  24. package/templates/host-swap/_variants/host.custom-page/index.html +12 -0
  25. package/templates/host-swap/_variants/host.custom-page/package.json +31 -0
  26. package/templates/host-swap/_variants/host.custom-page/src/App.tsx +14 -0
  27. package/templates/host-swap/_variants/host.custom-page/src/index.tsx +17 -0
  28. package/templates/host-swap/_variants/host.custom-page/tsconfig.json +19 -0
  29. package/templates/host-swap/_variants/host.custom-page/vite.config.ts +63 -0
  30. package/templates/host-swap/_variants/host.pcf/ControlManifest.Input.xml +17 -0
  31. package/templates/host-swap/_variants/host.pcf/featureconfig.json +3 -0
  32. package/templates/host-swap/_variants/host.pcf/gitignore +5 -0
  33. package/templates/host-swap/_variants/host.pcf/index.ts +44 -0
  34. package/templates/host-swap/_variants/host.pcf/package.json +31 -0
  35. package/templates/host-swap/_variants/host.pcf/pcf-env.d.ts +9 -0
  36. package/templates/host-swap/_variants/host.pcf/strings/{{componentName}}.1033.resx +31 -0
  37. package/templates/host-swap/_variants/host.pcf/tsconfig.json +9 -0
  38. package/templates/host-swap/src/Component.tsx +89 -0
  39. package/templates/host-swap/src/core/services/MockApiService.ts +58 -0
  40. package/templates/react-custom-page/src/core/services/MockApiService.ts +0 -4
  41. package/templates/grid-starter/src/core/services/XrmApiService.ts +0 -135
  42. package/templates/react-custom-page/src/core/services/FetchApiService.ts +0 -117
  43. package/templates/react-custom-page/src/core/services/IApiService.ts +0 -37
  44. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +0 -58
  45. package/templates/react-custom-page/src/core/services/crudLogging.ts +0 -52
  46. /package/templates/{grid-starter/src/core/services → _shared/dataverse-seam}/ServiceFactory.ts +0 -0
  47. /package/templates/{grid-starter/src/core/services → _shared/dataverse-seam}/crudLogging.ts +0 -0
@@ -1,117 +0,0 @@
1
- import { IApiService } from "./IApiService";
2
- import { logCrud } from "./crudLogging";
3
- import { logger } from "@khester/reusable-components";
4
-
5
- /**
6
- * Dev API service for token-proxy mode (`npm run dev` with DYNAMICS_URL set).
7
- * Requests go to the SAME-ORIGIN path /api/data/v9.2/... and the Vite dev proxy
8
- * (vite.config.ts) injects the `Authorization: Bearer` header server-side — the
9
- * token is never read by, or bundled into, the client.
10
- */
11
- export class FetchApiService implements IApiService {
12
- constructor(private baseUrl = "") {}
13
-
14
- private headers(): HeadersInit {
15
- return {
16
- "Content-Type": "application/json; charset=utf-8",
17
- Accept: "application/json",
18
- "OData-Version": "4.0",
19
- "OData-MaxVersion": "4.0",
20
- Prefer: 'odata.include-annotations="*"',
21
- };
22
- }
23
-
24
- retrieveMultipleRecords(
25
- entity: string,
26
- fetchXml: string,
27
- ): Promise<{ entities: any[] }> {
28
- return logCrud(
29
- { op: "READ", entity, resultCount: (r) => r?.entities?.length },
30
- async () => {
31
- const url = `${this.baseUrl}/api/data/v9.2/${entity}?fetchXml=${encodeURIComponent(fetchXml)}`;
32
- const response = await fetch(url, { method: "GET", headers: this.headers() });
33
- if (!response.ok) {
34
- const body = await response.text().catch(() => "");
35
- throw new Error(`API error ${response.status}: ${response.statusText}${body ? ` — ${body}` : ""}`);
36
- }
37
- const data = await response.json();
38
- return { entities: data.value ?? [] };
39
- },
40
- );
41
- }
42
-
43
- createRecord(entity: string, record: any): Promise<any> {
44
- return logCrud({ op: "CREATE", entity, resultId: (r) => r?.id }, async () => {
45
- const url = `${this.baseUrl}/api/data/v9.2/${entity}`;
46
- const response = await fetch(url, {
47
- method: "POST",
48
- headers: this.headers(),
49
- body: JSON.stringify(record),
50
- });
51
- if (!response.ok) {
52
- const body = await response.text().catch(() => "");
53
- throw new Error(`Create failed ${response.status}: ${body}`);
54
- }
55
- const entityId = response.headers.get("OData-EntityId");
56
- const match = entityId ? /\(([^)]+)\)/.exec(entityId) : null;
57
- return match ? { id: match[1] } : response.json().catch(() => ({}));
58
- });
59
- }
60
-
61
- updateRecord(entity: string, id: string, record: any): Promise<any> {
62
- return logCrud({ op: "UPDATE", entity, id }, async () => {
63
- const cleanId = id.replace(/[{}]/g, "");
64
- const url = `${this.baseUrl}/api/data/v9.2/${entity}(${cleanId})`;
65
- const response = await fetch(url, {
66
- method: "PATCH",
67
- headers: this.headers(),
68
- body: JSON.stringify(record),
69
- });
70
- if (!response.ok) {
71
- const body = await response.text().catch(() => "");
72
- throw new Error(`Update failed ${response.status}: ${body}`);
73
- }
74
- return { success: true };
75
- });
76
- }
77
-
78
- deleteRecord(entity: string, id: string): Promise<void> {
79
- return logCrud({ op: "DELETE", entity, id }, async () => {
80
- const cleanId = id.replace(/[{}]/g, "");
81
- const url = `${this.baseUrl}/api/data/v9.2/${entity}(${cleanId})`;
82
- const response = await fetch(url, { method: "DELETE", headers: this.headers() });
83
- if (!response.ok) {
84
- const body = await response.text().catch(() => "");
85
- throw new Error(`Delete failed ${response.status}: ${body}`);
86
- }
87
- });
88
- }
89
-
90
- executeRequest(requestName: string, requestData: any): Promise<any> {
91
- return logCrud({ op: "EXECUTE", entity: requestName }, async () => {
92
- const url = `${this.baseUrl}/api/data/v9.2/${requestName}`;
93
- const response = await fetch(url, {
94
- method: "POST",
95
- headers: this.headers(),
96
- body: JSON.stringify(requestData),
97
- });
98
- if (!response.ok) {
99
- const body = await response.text().catch(() => "");
100
- throw new Error(`Execute failed ${response.status}: ${body}`);
101
- }
102
- return response.json().catch(() => ({}));
103
- });
104
- }
105
-
106
- async uploadFile(file: File): Promise<string> {
107
- logger.warn("uploadFile not implemented", {
108
- source: "FetchApiService",
109
- data: { file: file.name },
110
- });
111
- return "";
112
- }
113
-
114
- async associateRecord(): Promise<void> {
115
- logger.warn("associateRecord not implemented", { source: "FetchApiService" });
116
- }
117
- }
@@ -1,37 +0,0 @@
1
- // The single data-access contract for the page. Three implementations satisfy
2
- // it — MockApiService (offline), FetchApiService (token-proxy dev), and
3
- // XrmApiService (production) — and ServiceFactory picks one per environment.
4
- //
5
- // Entities are addressed by their Web API ENTITY SET name (plural, e.g.
6
- // "accounts"), used consistently for reads and writes across all three services.
7
- export interface IApiService {
8
- /** Update a record. `record` contains only the changed attributes. */
9
- updateRecord(entity: string, id: string, record: any): Promise<any>;
10
-
11
- /** Create a record; resolves to `{ id }`. */
12
- createRecord(entity: string, record: any): Promise<any>;
13
-
14
- /** Delete a record. */
15
- deleteRecord(entity: string, id: string): Promise<void>;
16
-
17
- /** Retrieve records via a FetchXML query. */
18
- retrieveMultipleRecords(
19
- entity: string,
20
- fetchXml: string,
21
- ): Promise<{ entities: any[] }>;
22
-
23
- /** Execute a custom API / unbound function or action. */
24
- executeRequest(requestName: string, requestData: any): Promise<any>;
25
-
26
- /** Upload a file and return its URL. */
27
- uploadFile(file: File): Promise<string>;
28
-
29
- /** Associate two records via a relationship. */
30
- associateRecord(
31
- entityName: string,
32
- entityId: string,
33
- relationshipName: string,
34
- relatedEntityName: string,
35
- relatedEntityId: string,
36
- ): Promise<void>;
37
- }
@@ -1,58 +0,0 @@
1
- import { IApiService } from "./IApiService";
2
- import { MockApiService } from "./MockApiService";
3
- import { XrmApiService } from "./XrmApiService";
4
- import { FetchApiService } from "./FetchApiService";
5
- import { logger } from "@khester/reusable-components";
6
-
7
- /**
8
- * Single decision point for which IApiService the page talks to.
9
- *
10
- * Priority:
11
- * 1. localhost + VITE_USE_PROXY → FetchApiService (real Dataverse via the Vite proxy)
12
- * 2. localhost (no proxy) → injected mock, or MockApiService by default
13
- * 3. deployed + Xrm context → XrmApiService
14
- */
15
- export class ServiceFactory {
16
- public static isMockEnvironment: boolean =
17
- window.location.hostname === "localhost" ||
18
- window.location.hostname === "127.0.0.1";
19
-
20
- /**
21
- * Token-proxy dev mode. `vite.config.ts` sets `import.meta.env.VITE_USE_PROXY`
22
- * to a boolean — true when DYNAMICS_URL is configured — and proxies
23
- * /api/data/* to Dataverse with a server-side bearer token. Truthiness check
24
- * (NOT `=== 'true'`): the `define` value is a real boolean, not a string.
25
- */
26
- public static isTokenMode = !!import.meta.env.VITE_USE_PROXY;
27
-
28
- /**
29
- * @param Xrm The Xrm context (required when deployed). Resolve it from
30
- * `window.parent?.Xrm ?? window.Xrm` in App.tsx.
31
- * @param createMock Optional factory for a domain-specific mock. Used ONLY in
32
- * mock mode (after the token-mode check), so a token session
33
- * still talks to the real org. Defaults to MockApiService.
34
- */
35
- public static createApiService(
36
- Xrm?: any,
37
- createMock?: () => IApiService,
38
- ): IApiService {
39
- if (ServiceFactory.isMockEnvironment && ServiceFactory.isTokenMode) {
40
- logger.info("Token mode — using FetchApiService via proxy", {
41
- source: "ServiceFactory",
42
- });
43
- return new FetchApiService();
44
- }
45
-
46
- if (ServiceFactory.isMockEnvironment) {
47
- logger.info("Mock mode — using MockApiService", {
48
- source: "ServiceFactory",
49
- });
50
- return (createMock ?? (() => new MockApiService()))();
51
- }
52
-
53
- if (!Xrm) {
54
- throw new Error("Xrm object is required in a non-mock environment.");
55
- }
56
- return new XrmApiService(Xrm);
57
- }
58
- }
@@ -1,52 +0,0 @@
1
- // Binds the generic `withCrudLog` wrapper (from @khester/reusable-components) to
2
- // the Dataverse error shapes thrown by the IApiService implementations, so each
3
- // service method can be instrumented with a single `logCrud(meta, fn)` call.
4
- //
5
- // `parseServiceError` lives here (app/template code), not in the library — the
6
- // library's logger stays backend-agnostic and the app injects this parser.
7
-
8
- import { withCrudLog, type CrudMeta } from "@khester/reusable-components";
9
-
10
- /**
11
- * Extract a numeric status + human message from the error shapes the services
12
- * throw: an `Error("… 400: <body>")` whose body may embed a Dataverse error
13
- * JSON blob, or a plain `json.error` object with a CDS InnerError message.
14
- */
15
- function parseServiceError(err: unknown): { status?: number; message?: string } {
16
- if (err && typeof err === "object" && !(err instanceof Error)) {
17
- const obj = err as Record<string, unknown>;
18
- const inner = obj["@Microsoft.PowerApps.CDS.InnerError.Message"];
19
- const message =
20
- (typeof inner === "string" && inner) ||
21
- (typeof obj.message === "string" && obj.message) ||
22
- undefined;
23
- const status = typeof obj.status === "number" ? obj.status : undefined;
24
- return { status, message: message || undefined };
25
- }
26
-
27
- if (err instanceof Error) {
28
- const statusMatch = err.message.match(/\b(\d{3})\b/);
29
- const status = statusMatch ? Number(statusMatch[1]) : undefined;
30
-
31
- const jsonStart = err.message.indexOf("{");
32
- if (jsonStart !== -1) {
33
- try {
34
- const parsed = JSON.parse(err.message.substring(jsonStart));
35
- const inner =
36
- parsed?.error?.["@Microsoft.PowerApps.CDS.InnerError.Message"];
37
- const message = inner || parsed?.error?.message || err.message;
38
- return { status, message };
39
- } catch {
40
- // not JSON — fall through to the raw message
41
- }
42
- }
43
- return { status, message: err.message };
44
- }
45
-
46
- return { message: String(err) };
47
- }
48
-
49
- /** Run a CRUD operation, emitting one structured `[CRUD] …` log line for it. */
50
- export function logCrud<T>(meta: CrudMeta, fn: () => Promise<T>): Promise<T> {
51
- return withCrudLog(meta, fn, { parseError: parseServiceError });
52
- }