@khester/dataverse-codegen 0.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/README.md +133 -0
- package/dist/cli.cjs +5481 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +5488 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +1428 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +241 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.mjs +1406 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
11
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
// ../../node_modules/tsup/assets/esm_shims.js
|
|
31
|
+
import path from "path";
|
|
32
|
+
import { fileURLToPath } from "url";
|
|
33
|
+
var init_esm_shims = __esm({
|
|
34
|
+
"../../node_modules/tsup/assets/esm_shims.js"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ../api-client/dist/index.js
|
|
40
|
+
var require_dist = __commonJS({
|
|
41
|
+
"../api-client/dist/index.js"(exports, module) {
|
|
42
|
+
"use strict";
|
|
43
|
+
init_esm_shims();
|
|
44
|
+
var __defProp2 = Object.defineProperty;
|
|
45
|
+
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
|
46
|
+
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
|
47
|
+
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
|
48
|
+
var __export = (target, all) => {
|
|
49
|
+
for (var name in all)
|
|
50
|
+
__defProp2(target, name, { get: all[name], enumerable: true });
|
|
51
|
+
};
|
|
52
|
+
var __copyProps2 = (to, from, except, desc) => {
|
|
53
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
54
|
+
for (let key of __getOwnPropNames2(from))
|
|
55
|
+
if (!__hasOwnProp2.call(to, key) && key !== except)
|
|
56
|
+
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
|
|
57
|
+
}
|
|
58
|
+
return to;
|
|
59
|
+
};
|
|
60
|
+
var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
|
|
61
|
+
var index_exports = {};
|
|
62
|
+
__export(index_exports, {
|
|
63
|
+
FetchClient: () => FetchClient,
|
|
64
|
+
MockClient: () => MockClient,
|
|
65
|
+
XrmClient: () => XrmClient,
|
|
66
|
+
createClient: () => createClient2,
|
|
67
|
+
getEnvironmentInfo: () => getEnvironmentInfo
|
|
68
|
+
});
|
|
69
|
+
module.exports = __toCommonJS(index_exports);
|
|
70
|
+
var FetchClient = class {
|
|
71
|
+
constructor(baseUrl, token, logger) {
|
|
72
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
73
|
+
this.token = token;
|
|
74
|
+
this.log = logger ?? console;
|
|
75
|
+
}
|
|
76
|
+
/** Update the token (e.g., after refresh) */
|
|
77
|
+
setToken(token) {
|
|
78
|
+
this.token = token;
|
|
79
|
+
}
|
|
80
|
+
getHeaders() {
|
|
81
|
+
return {
|
|
82
|
+
"Authorization": `Bearer ${this.token}`,
|
|
83
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
84
|
+
"Accept": "application/json",
|
|
85
|
+
"OData-MaxVersion": "4.0",
|
|
86
|
+
"OData-Version": "4.0",
|
|
87
|
+
"Prefer": 'odata.include-annotations="*"'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
buildUrl(path2, id, options) {
|
|
91
|
+
let url = `${this.baseUrl}/api/data/v9.2/${path2}`;
|
|
92
|
+
if (id) {
|
|
93
|
+
url += `(${id.replace(/[{}]/g, "")})`;
|
|
94
|
+
}
|
|
95
|
+
if (options) {
|
|
96
|
+
url += options.startsWith("?") ? options : `?${options}`;
|
|
97
|
+
}
|
|
98
|
+
return url;
|
|
99
|
+
}
|
|
100
|
+
async handleResponse(response) {
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
103
|
+
try {
|
|
104
|
+
const errorBody = await response.json();
|
|
105
|
+
if (errorBody.error?.message) {
|
|
106
|
+
errorMessage = errorBody.error.message;
|
|
107
|
+
} else if (errorBody.Message) {
|
|
108
|
+
errorMessage = errorBody.Message;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
if (response.status === 401) {
|
|
113
|
+
errorMessage += " (Token may be expired)";
|
|
114
|
+
} else if (response.status === 403) {
|
|
115
|
+
errorMessage += " (Insufficient permissions)";
|
|
116
|
+
} else if (response.status === 404) {
|
|
117
|
+
errorMessage += " (Entity or record not found)";
|
|
118
|
+
}
|
|
119
|
+
throw new Error(errorMessage);
|
|
120
|
+
}
|
|
121
|
+
if (response.status === 204) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
return response.json();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* GET a collection endpoint, following `@odata.nextLink` so wide results
|
|
128
|
+
* (e.g. an entity with hundreds of attributes, or large metadata sets) are
|
|
129
|
+
* not silently truncated at the server page cap.
|
|
130
|
+
*/
|
|
131
|
+
async fetchAllPages(url) {
|
|
132
|
+
const out = [];
|
|
133
|
+
let next = url;
|
|
134
|
+
while (next) {
|
|
135
|
+
const response = await fetch(next, { method: "GET", headers: this.getHeaders() });
|
|
136
|
+
const page = await this.handleResponse(response);
|
|
137
|
+
if (page.value) out.push(...page.value);
|
|
138
|
+
next = page["@odata.nextLink"];
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
extractIdFromResponse(response, _entitySetName) {
|
|
143
|
+
const entityIdHeader = response.headers.get("OData-EntityId");
|
|
144
|
+
if (entityIdHeader) {
|
|
145
|
+
const match = entityIdHeader.match(/\(([0-9a-f-]+)\)/i);
|
|
146
|
+
if (match) return match[1];
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// CRUD Operations
|
|
151
|
+
async retrieve(entitySetName, id, options) {
|
|
152
|
+
const url = this.buildUrl(entitySetName, id, options);
|
|
153
|
+
this.log.log(`[FetchClient] GET ${url}`);
|
|
154
|
+
const response = await fetch(url, { method: "GET", headers: this.getHeaders() });
|
|
155
|
+
return this.handleResponse(response);
|
|
156
|
+
}
|
|
157
|
+
async retrieveMultiple(entitySetName, options) {
|
|
158
|
+
const url = this.buildUrl(entitySetName, void 0, options);
|
|
159
|
+
this.log.log(`[FetchClient] GET ${url}`);
|
|
160
|
+
const response = await fetch(url, { method: "GET", headers: this.getHeaders() });
|
|
161
|
+
return this.handleResponse(response);
|
|
162
|
+
}
|
|
163
|
+
async create(entitySetName, data) {
|
|
164
|
+
const url = this.buildUrl(entitySetName);
|
|
165
|
+
this.log.log(`[FetchClient] POST ${url}`);
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { ...this.getHeaders(), "Prefer": "return=representation" },
|
|
169
|
+
body: JSON.stringify(data)
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
return this.handleResponse(response);
|
|
173
|
+
}
|
|
174
|
+
const id = this.extractIdFromResponse(response, entitySetName);
|
|
175
|
+
if (id) return { id };
|
|
176
|
+
try {
|
|
177
|
+
const body = await response.json();
|
|
178
|
+
const primaryKey = entitySetName.slice(0, -1) + "id";
|
|
179
|
+
const recordId = body[primaryKey] || body.id;
|
|
180
|
+
if (recordId) return { id: recordId };
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Could not determine created record ID");
|
|
184
|
+
}
|
|
185
|
+
async update(entitySetName, id, data) {
|
|
186
|
+
const url = this.buildUrl(entitySetName, id);
|
|
187
|
+
this.log.log(`[FetchClient] PATCH ${url}`);
|
|
188
|
+
const response = await fetch(url, {
|
|
189
|
+
method: "PATCH",
|
|
190
|
+
headers: this.getHeaders(),
|
|
191
|
+
body: JSON.stringify(data)
|
|
192
|
+
});
|
|
193
|
+
await this.handleResponse(response);
|
|
194
|
+
}
|
|
195
|
+
async delete(entitySetName, id) {
|
|
196
|
+
const url = this.buildUrl(entitySetName, id);
|
|
197
|
+
this.log.log(`[FetchClient] DELETE ${url}`);
|
|
198
|
+
const response = await fetch(url, { method: "DELETE", headers: this.getHeaders() });
|
|
199
|
+
await this.handleResponse(response);
|
|
200
|
+
}
|
|
201
|
+
// Batch Operations
|
|
202
|
+
async batchDelete(entitySetName, ids) {
|
|
203
|
+
const succeeded = [];
|
|
204
|
+
const failed = [];
|
|
205
|
+
for (const id of ids) {
|
|
206
|
+
try {
|
|
207
|
+
await this.delete(entitySetName, id);
|
|
208
|
+
succeeded.push(id);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
failed.push({ id, error: err instanceof Error ? err.message : "Delete failed" });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { succeeded, failed };
|
|
214
|
+
}
|
|
215
|
+
async batchUpdate(entitySetName, records) {
|
|
216
|
+
const succeeded = [];
|
|
217
|
+
const failed = [];
|
|
218
|
+
for (const record of records) {
|
|
219
|
+
try {
|
|
220
|
+
await this.update(entitySetName, record.id, record.data);
|
|
221
|
+
succeeded.push(record.id);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
failed.push({ id: record.id, error: err instanceof Error ? err.message : "Update failed" });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { succeeded, failed };
|
|
227
|
+
}
|
|
228
|
+
async batchCreate(entitySetName, records) {
|
|
229
|
+
const succeeded = [];
|
|
230
|
+
const failed = [];
|
|
231
|
+
for (let i = 0; i < records.length; i++) {
|
|
232
|
+
try {
|
|
233
|
+
const result = await this.create(entitySetName, records[i]);
|
|
234
|
+
succeeded.push({ id: result.id, data: records[i] });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
failed.push({ rowNumber: i + 1, error: err instanceof Error ? err.message : "Create failed" });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { succeeded, failed };
|
|
240
|
+
}
|
|
241
|
+
// Metadata Operations
|
|
242
|
+
async getEntityDefinitions(filter) {
|
|
243
|
+
const selectFields = [
|
|
244
|
+
"LogicalName",
|
|
245
|
+
"EntitySetName",
|
|
246
|
+
"SchemaName",
|
|
247
|
+
"DisplayName",
|
|
248
|
+
"DisplayCollectionName",
|
|
249
|
+
"PrimaryIdAttribute",
|
|
250
|
+
"PrimaryNameAttribute",
|
|
251
|
+
"IsCustomEntity",
|
|
252
|
+
"IsActivity",
|
|
253
|
+
"IsValidForAdvancedFind",
|
|
254
|
+
"ObjectTypeCode"
|
|
255
|
+
].join(",");
|
|
256
|
+
let options = `?$select=${selectFields}`;
|
|
257
|
+
if (filter) {
|
|
258
|
+
options += `&$filter=${encodeURIComponent(filter)}`;
|
|
259
|
+
}
|
|
260
|
+
const url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions${options}`;
|
|
261
|
+
this.log.log(`[FetchClient] GET ${url}`);
|
|
262
|
+
return this.fetchAllPages(url);
|
|
263
|
+
}
|
|
264
|
+
async getAttributeMetadata(entityLogicalName, filter) {
|
|
265
|
+
const selectFields = [
|
|
266
|
+
"LogicalName",
|
|
267
|
+
"SchemaName",
|
|
268
|
+
"DisplayName",
|
|
269
|
+
"Description",
|
|
270
|
+
"AttributeType",
|
|
271
|
+
"AttributeTypeName",
|
|
272
|
+
"RequiredLevel",
|
|
273
|
+
"IsValidForRead",
|
|
274
|
+
"IsValidForCreate",
|
|
275
|
+
"IsValidForUpdate",
|
|
276
|
+
"IsPrimaryId",
|
|
277
|
+
"IsPrimaryName"
|
|
278
|
+
].join(",");
|
|
279
|
+
const defaultFilter = "IsValidForRead eq true";
|
|
280
|
+
const combinedFilter = filter ? `${defaultFilter} and ${filter}` : defaultFilter;
|
|
281
|
+
const url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes?$select=${selectFields}&$filter=${encodeURIComponent(combinedFilter)}`;
|
|
282
|
+
this.log.log(`[FetchClient] GET ${url}`);
|
|
283
|
+
const baseAttributes = await this.fetchAllPages(url);
|
|
284
|
+
const attributesWithExtras = await Promise.all(
|
|
285
|
+
baseAttributes.map(async (attr) => {
|
|
286
|
+
if (attr.AttributeType === "Picklist" || attr.AttributeType === "State" || attr.AttributeType === "Status" || attr.AttributeType === "MultiSelectPicklist") {
|
|
287
|
+
try {
|
|
288
|
+
const castType = attr.AttributeType === "State" ? "StateAttributeMetadata" : attr.AttributeType === "Status" ? "StatusAttributeMetadata" : attr.AttributeType === "MultiSelectPicklist" ? "MultiSelectPicklistAttributeMetadata" : "PicklistAttributeMetadata";
|
|
289
|
+
const optionSetUrl = `${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(LogicalName='${attr.LogicalName}')/Microsoft.Dynamics.CRM.${castType}?$select=LogicalName&$expand=OptionSet`;
|
|
290
|
+
const optionResponse = await fetch(optionSetUrl, { method: "GET", headers: this.getHeaders() });
|
|
291
|
+
if (optionResponse.ok) {
|
|
292
|
+
const optionResult = await optionResponse.json();
|
|
293
|
+
return { ...attr, OptionSet: optionResult.OptionSet };
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.log.warn(`[FetchClient] Could not fetch options for ${attr.LogicalName}:`, err);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (attr.AttributeType === "Lookup") {
|
|
300
|
+
try {
|
|
301
|
+
const lookupUrl = `${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(LogicalName='${attr.LogicalName}')/Microsoft.Dynamics.CRM.LookupAttributeMetadata?$select=LogicalName,Targets`;
|
|
302
|
+
const lookupResponse = await fetch(lookupUrl, { method: "GET", headers: this.getHeaders() });
|
|
303
|
+
if (lookupResponse.ok) {
|
|
304
|
+
const lookupResult = await lookupResponse.json();
|
|
305
|
+
return { ...attr, Targets: lookupResult.Targets };
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
this.log.warn(`[FetchClient] Could not fetch targets for ${attr.LogicalName}:`, err);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return attr;
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
return attributesWithExtras;
|
|
315
|
+
}
|
|
316
|
+
async getRelationshipMetadata(entityLogicalName) {
|
|
317
|
+
const selectFields = [
|
|
318
|
+
"MetadataId",
|
|
319
|
+
"SchemaName",
|
|
320
|
+
"RelationshipType",
|
|
321
|
+
"ReferencedEntity",
|
|
322
|
+
"ReferencedAttribute",
|
|
323
|
+
"ReferencingEntity",
|
|
324
|
+
"ReferencingAttribute",
|
|
325
|
+
"IsCustomRelationship",
|
|
326
|
+
"ReferencingEntityNavigationPropertyName",
|
|
327
|
+
"ReferencedEntityNavigationPropertyName"
|
|
328
|
+
].join(",");
|
|
329
|
+
const [oneToManyResponse, manyToOneResponse] = await Promise.all([
|
|
330
|
+
fetch(
|
|
331
|
+
`${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/OneToManyRelationships?$select=${selectFields}`,
|
|
332
|
+
{ method: "GET", headers: this.getHeaders() }
|
|
333
|
+
),
|
|
334
|
+
fetch(
|
|
335
|
+
`${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/ManyToOneRelationships?$select=${selectFields}`,
|
|
336
|
+
{ method: "GET", headers: this.getHeaders() }
|
|
337
|
+
)
|
|
338
|
+
]);
|
|
339
|
+
const oneToMany = await this.handleResponse(oneToManyResponse);
|
|
340
|
+
const manyToOne = await this.handleResponse(manyToOneResponse);
|
|
341
|
+
this.log.log(`[FetchClient] Loaded ${oneToMany.value.length} OneToMany and ${manyToOne.value.length} ManyToOne relationships for ${entityLogicalName}`);
|
|
342
|
+
return [...oneToMany.value, ...manyToOne.value];
|
|
343
|
+
}
|
|
344
|
+
// View Operations
|
|
345
|
+
async getViews(entityLogicalName) {
|
|
346
|
+
const selectFields = "savedqueryid,name,fetchxml,layoutxml,isdefault,returnedtypecode,description";
|
|
347
|
+
const userQuerySelectFields = "userqueryid,name,fetchxml,layoutxml,returnedtypecode,description";
|
|
348
|
+
const [systemViewsResponse, personalViewsResponse] = await Promise.all([
|
|
349
|
+
fetch(
|
|
350
|
+
`${this.baseUrl}/api/data/v9.2/savedqueries?$select=${selectFields}&$filter=returnedtypecode eq '${entityLogicalName}' and querytype eq 0 and statecode eq 0`,
|
|
351
|
+
{ method: "GET", headers: this.getHeaders() }
|
|
352
|
+
),
|
|
353
|
+
fetch(
|
|
354
|
+
`${this.baseUrl}/api/data/v9.2/userqueries?$select=${userQuerySelectFields}&$filter=returnedtypecode eq '${entityLogicalName}' and statecode eq 0`,
|
|
355
|
+
{ method: "GET", headers: this.getHeaders() }
|
|
356
|
+
)
|
|
357
|
+
]);
|
|
358
|
+
const systemViews = await this.handleResponse(systemViewsResponse);
|
|
359
|
+
const personalViews = await this.handleResponse(personalViewsResponse);
|
|
360
|
+
const views = [
|
|
361
|
+
...systemViews.value.map((v) => ({
|
|
362
|
+
id: v.savedqueryid,
|
|
363
|
+
name: v.name,
|
|
364
|
+
fetchxml: v.fetchxml,
|
|
365
|
+
layoutxml: v.layoutxml,
|
|
366
|
+
isDefault: v.isdefault || false,
|
|
367
|
+
isPersonal: false,
|
|
368
|
+
returnedtypecode: v.returnedtypecode,
|
|
369
|
+
description: v.description
|
|
370
|
+
})),
|
|
371
|
+
...personalViews.value.map((v) => ({
|
|
372
|
+
id: v.userqueryid,
|
|
373
|
+
name: v.name,
|
|
374
|
+
fetchxml: v.fetchxml,
|
|
375
|
+
layoutxml: v.layoutxml,
|
|
376
|
+
isDefault: false,
|
|
377
|
+
isPersonal: true,
|
|
378
|
+
returnedtypecode: v.returnedtypecode,
|
|
379
|
+
description: v.description
|
|
380
|
+
}))
|
|
381
|
+
];
|
|
382
|
+
views.sort((a, b) => {
|
|
383
|
+
if (a.isDefault && !b.isDefault) return -1;
|
|
384
|
+
if (!a.isDefault && b.isDefault) return 1;
|
|
385
|
+
return a.name.localeCompare(b.name);
|
|
386
|
+
});
|
|
387
|
+
return views;
|
|
388
|
+
}
|
|
389
|
+
async updateView(isPersonal, viewId, data) {
|
|
390
|
+
const entitySet = isPersonal ? "userqueries" : "savedqueries";
|
|
391
|
+
const url = this.buildUrl(entitySet, viewId);
|
|
392
|
+
this.log.log(`[FetchClient] PATCH ${url}`);
|
|
393
|
+
const response = await fetch(url, { method: "PATCH", headers: this.getHeaders(), body: JSON.stringify(data) });
|
|
394
|
+
await this.handleResponse(response);
|
|
395
|
+
}
|
|
396
|
+
async createView(entityLogicalName, data) {
|
|
397
|
+
const url = this.buildUrl("userqueries");
|
|
398
|
+
const payload = {
|
|
399
|
+
name: data.name,
|
|
400
|
+
returnedtypecode: entityLogicalName,
|
|
401
|
+
fetchxml: data.fetchxml,
|
|
402
|
+
layoutxml: data.layoutxml,
|
|
403
|
+
querytype: 0,
|
|
404
|
+
statecode: 0,
|
|
405
|
+
...data.description && { description: data.description }
|
|
406
|
+
};
|
|
407
|
+
this.log.log(`[FetchClient] POST ${url}`);
|
|
408
|
+
const response = await fetch(url, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: { ...this.getHeaders(), "Prefer": "return=representation" },
|
|
411
|
+
body: JSON.stringify(payload)
|
|
412
|
+
});
|
|
413
|
+
if (!response.ok) return this.handleResponse(response);
|
|
414
|
+
const id = this.extractIdFromResponse(response, "userqueries");
|
|
415
|
+
if (id) return { id };
|
|
416
|
+
try {
|
|
417
|
+
const body = await response.json();
|
|
418
|
+
if (body.userqueryid || body.id) return { id: body.userqueryid || body.id };
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
throw new Error("Could not determine created view ID");
|
|
422
|
+
}
|
|
423
|
+
async deleteView(isPersonal, viewId) {
|
|
424
|
+
const entitySet = isPersonal ? "userqueries" : "savedqueries";
|
|
425
|
+
await this.delete(entitySet, viewId);
|
|
426
|
+
}
|
|
427
|
+
async publishEntity(entityLogicalName) {
|
|
428
|
+
const url = `${this.baseUrl}/api/data/v9.2/PublishXml`;
|
|
429
|
+
const parameterXml = `<importexportxml><entities><entity>${entityLogicalName}</entity></entities></importexportxml>`;
|
|
430
|
+
this.log.log(`[FetchClient] POST ${url} - Publishing ${entityLogicalName}`);
|
|
431
|
+
const response = await fetch(url, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: this.getHeaders(),
|
|
434
|
+
body: JSON.stringify({ ParameterXml: parameterXml })
|
|
435
|
+
});
|
|
436
|
+
await this.handleResponse(response);
|
|
437
|
+
}
|
|
438
|
+
// Global Option Set Operations
|
|
439
|
+
async getGlobalOptionSets() {
|
|
440
|
+
const url = `${this.baseUrl}/api/data/v9.2/GlobalOptionSetDefinitions`;
|
|
441
|
+
this.log.log(`[FetchClient] GET ${url}`);
|
|
442
|
+
const all = await this.fetchAllPages(url);
|
|
443
|
+
return all.filter((os) => os.OptionSetType === "Picklist" && os.IsGlobal);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var XrmClient = class {
|
|
447
|
+
constructor(xrm, logger) {
|
|
448
|
+
this.xrm = xrm;
|
|
449
|
+
this.log = logger ?? console;
|
|
450
|
+
}
|
|
451
|
+
getClientUrl() {
|
|
452
|
+
return this.xrm.Utility.getGlobalContext().getClientUrl();
|
|
453
|
+
}
|
|
454
|
+
// CRUD Operations
|
|
455
|
+
async retrieve(entitySetName, id, options) {
|
|
456
|
+
const cleanId = id.replace(/[{}]/g, "");
|
|
457
|
+
const result = await this.xrm.WebApi.retrieveRecord(entitySetName, cleanId, options);
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
async retrieveMultiple(entitySetName, options) {
|
|
461
|
+
const result = await this.xrm.WebApi.retrieveMultipleRecords(entitySetName, options);
|
|
462
|
+
return { value: result.entities };
|
|
463
|
+
}
|
|
464
|
+
async create(entitySetName, data) {
|
|
465
|
+
const result = await this.xrm.WebApi.createRecord(entitySetName, data);
|
|
466
|
+
return { id: result.id };
|
|
467
|
+
}
|
|
468
|
+
async update(entitySetName, id, data) {
|
|
469
|
+
await this.xrm.WebApi.updateRecord(entitySetName, id.replace(/[{}]/g, ""), data);
|
|
470
|
+
}
|
|
471
|
+
async delete(entitySetName, id) {
|
|
472
|
+
await this.xrm.WebApi.deleteRecord(entitySetName, id.replace(/[{}]/g, ""));
|
|
473
|
+
}
|
|
474
|
+
// Batch Operations
|
|
475
|
+
async batchDelete(entitySetName, ids) {
|
|
476
|
+
const succeeded = [];
|
|
477
|
+
const failed = [];
|
|
478
|
+
for (const id of ids) {
|
|
479
|
+
try {
|
|
480
|
+
await this.delete(entitySetName, id);
|
|
481
|
+
succeeded.push(id);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
failed.push({ id, error: err instanceof Error ? err.message : "Delete failed" });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { succeeded, failed };
|
|
487
|
+
}
|
|
488
|
+
async batchUpdate(entitySetName, records) {
|
|
489
|
+
const succeeded = [];
|
|
490
|
+
const failed = [];
|
|
491
|
+
for (const record of records) {
|
|
492
|
+
try {
|
|
493
|
+
await this.update(entitySetName, record.id, record.data);
|
|
494
|
+
succeeded.push(record.id);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
failed.push({ id: record.id, error: err instanceof Error ? err.message : "Update failed" });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { succeeded, failed };
|
|
500
|
+
}
|
|
501
|
+
async batchCreate(entitySetName, records) {
|
|
502
|
+
const succeeded = [];
|
|
503
|
+
const failed = [];
|
|
504
|
+
for (let i = 0; i < records.length; i++) {
|
|
505
|
+
try {
|
|
506
|
+
const result = await this.create(entitySetName, records[i]);
|
|
507
|
+
succeeded.push({ id: result.id, data: records[i] });
|
|
508
|
+
} catch (err) {
|
|
509
|
+
failed.push({ rowNumber: i + 1, error: err instanceof Error ? err.message : "Create failed" });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return { succeeded, failed };
|
|
513
|
+
}
|
|
514
|
+
// Metadata Operations
|
|
515
|
+
async getEntityDefinitions(filter) {
|
|
516
|
+
const selectFields = [
|
|
517
|
+
"LogicalName",
|
|
518
|
+
"EntitySetName",
|
|
519
|
+
"SchemaName",
|
|
520
|
+
"DisplayName",
|
|
521
|
+
"DisplayCollectionName",
|
|
522
|
+
"PrimaryIdAttribute",
|
|
523
|
+
"PrimaryNameAttribute",
|
|
524
|
+
"IsCustomEntity",
|
|
525
|
+
"IsActivity",
|
|
526
|
+
"IsValidForAdvancedFind",
|
|
527
|
+
"ObjectTypeCode"
|
|
528
|
+
].join(",");
|
|
529
|
+
let options = `?$select=${selectFields}`;
|
|
530
|
+
if (filter) options += `&$filter=${filter}`;
|
|
531
|
+
const result = await this.xrm.WebApi.retrieveMultipleRecords("EntityDefinitions", options);
|
|
532
|
+
return result.entities;
|
|
533
|
+
}
|
|
534
|
+
async getAttributeMetadata(entityLogicalName, filter) {
|
|
535
|
+
const selectFields = [
|
|
536
|
+
"LogicalName",
|
|
537
|
+
"SchemaName",
|
|
538
|
+
"DisplayName",
|
|
539
|
+
"Description",
|
|
540
|
+
"AttributeType",
|
|
541
|
+
"AttributeTypeName",
|
|
542
|
+
"RequiredLevel",
|
|
543
|
+
"IsValidForRead",
|
|
544
|
+
"IsValidForCreate",
|
|
545
|
+
"IsValidForUpdate",
|
|
546
|
+
"IsPrimaryId",
|
|
547
|
+
"IsPrimaryName"
|
|
548
|
+
].join(",");
|
|
549
|
+
const defaultFilter = "IsValidForRead eq true";
|
|
550
|
+
const combinedFilter = filter ? `${defaultFilter} and ${filter}` : defaultFilter;
|
|
551
|
+
const clientUrl = this.getClientUrl();
|
|
552
|
+
const url = `${clientUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes?$select=${selectFields}&$filter=${combinedFilter}`;
|
|
553
|
+
const response = await fetch(url, {
|
|
554
|
+
method: "GET",
|
|
555
|
+
headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" }
|
|
556
|
+
});
|
|
557
|
+
if (!response.ok) throw new Error(`Failed to fetch attribute metadata: ${response.statusText}`);
|
|
558
|
+
const result = await response.json();
|
|
559
|
+
const attributes = result.value;
|
|
560
|
+
const attributesWithExtras = await Promise.all(
|
|
561
|
+
attributes.map(async (attr) => {
|
|
562
|
+
if (attr.AttributeType === "Picklist" || attr.AttributeType === "State" || attr.AttributeType === "Status") {
|
|
563
|
+
try {
|
|
564
|
+
const castType = attr.AttributeType === "State" ? "StateAttributeMetadata" : attr.AttributeType === "Status" ? "StatusAttributeMetadata" : "PicklistAttributeMetadata";
|
|
565
|
+
const optionSetUrl = `${clientUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(LogicalName='${attr.LogicalName}')/Microsoft.Dynamics.CRM.${castType}?$select=LogicalName&$expand=OptionSet`;
|
|
566
|
+
const optionResponse = await fetch(optionSetUrl, {
|
|
567
|
+
method: "GET",
|
|
568
|
+
headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" }
|
|
569
|
+
});
|
|
570
|
+
if (optionResponse.ok) {
|
|
571
|
+
const optionResult = await optionResponse.json();
|
|
572
|
+
return { ...attr, OptionSet: optionResult.OptionSet };
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
this.log.warn(`Could not fetch options for ${attr.LogicalName}:`, err);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (attr.AttributeType === "Lookup") {
|
|
579
|
+
try {
|
|
580
|
+
const lookupUrl = `${clientUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes(LogicalName='${attr.LogicalName}')/Microsoft.Dynamics.CRM.LookupAttributeMetadata?$select=LogicalName,Targets`;
|
|
581
|
+
const lookupResponse = await fetch(lookupUrl, {
|
|
582
|
+
method: "GET",
|
|
583
|
+
headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" }
|
|
584
|
+
});
|
|
585
|
+
if (lookupResponse.ok) {
|
|
586
|
+
const lookupResult = await lookupResponse.json();
|
|
587
|
+
return { ...attr, Targets: lookupResult.Targets };
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
this.log.warn(`Could not fetch targets for ${attr.LogicalName}:`, err);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return attr;
|
|
594
|
+
})
|
|
595
|
+
);
|
|
596
|
+
return attributesWithExtras;
|
|
597
|
+
}
|
|
598
|
+
async getRelationshipMetadata(entityLogicalName) {
|
|
599
|
+
const clientUrl = this.getClientUrl();
|
|
600
|
+
const selectFields = [
|
|
601
|
+
"MetadataId",
|
|
602
|
+
"SchemaName",
|
|
603
|
+
"RelationshipType",
|
|
604
|
+
"ReferencedEntity",
|
|
605
|
+
"ReferencedAttribute",
|
|
606
|
+
"ReferencingEntity",
|
|
607
|
+
"ReferencingAttribute",
|
|
608
|
+
"IsCustomRelationship",
|
|
609
|
+
"ReferencingEntityNavigationPropertyName",
|
|
610
|
+
"ReferencedEntityNavigationPropertyName"
|
|
611
|
+
].join(",");
|
|
612
|
+
const [oneToManyResponse, manyToOneResponse] = await Promise.all([
|
|
613
|
+
fetch(
|
|
614
|
+
`${clientUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/OneToManyRelationships?$select=${selectFields}`,
|
|
615
|
+
{ method: "GET", headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" } }
|
|
616
|
+
),
|
|
617
|
+
fetch(
|
|
618
|
+
`${clientUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/ManyToOneRelationships?$select=${selectFields}`,
|
|
619
|
+
{ method: "GET", headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" } }
|
|
620
|
+
)
|
|
621
|
+
]);
|
|
622
|
+
if (!oneToManyResponse.ok) throw new Error(`Failed to fetch OneToMany relationships: ${oneToManyResponse.statusText}`);
|
|
623
|
+
if (!manyToOneResponse.ok) throw new Error(`Failed to fetch ManyToOne relationships: ${manyToOneResponse.statusText}`);
|
|
624
|
+
const oneToMany = await oneToManyResponse.json();
|
|
625
|
+
const manyToOne = await manyToOneResponse.json();
|
|
626
|
+
this.log.log(`[XrmClient] Loaded ${oneToMany.value.length} OneToMany and ${manyToOne.value.length} ManyToOne relationships for ${entityLogicalName}`);
|
|
627
|
+
return [...oneToMany.value, ...manyToOne.value];
|
|
628
|
+
}
|
|
629
|
+
// View Operations
|
|
630
|
+
async getViews(entityLogicalName) {
|
|
631
|
+
const clientUrl = this.getClientUrl();
|
|
632
|
+
const selectFields = "savedqueryid,name,fetchxml,layoutxml,isdefault,returnedtypecode,description";
|
|
633
|
+
const userQuerySelectFields = "userqueryid,name,fetchxml,layoutxml,returnedtypecode,description";
|
|
634
|
+
const [systemViewsResponse, personalViewsResponse] = await Promise.all([
|
|
635
|
+
fetch(
|
|
636
|
+
`${clientUrl}/api/data/v9.2/savedqueries?$select=${selectFields}&$filter=returnedtypecode eq '${entityLogicalName}' and querytype eq 0 and statecode eq 0`,
|
|
637
|
+
{ method: "GET", headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" } }
|
|
638
|
+
),
|
|
639
|
+
fetch(
|
|
640
|
+
`${clientUrl}/api/data/v9.2/userqueries?$select=${userQuerySelectFields}&$filter=returnedtypecode eq '${entityLogicalName}' and statecode eq 0`,
|
|
641
|
+
{ method: "GET", headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" } }
|
|
642
|
+
)
|
|
643
|
+
]);
|
|
644
|
+
if (!systemViewsResponse.ok) throw new Error(`Failed to fetch system views: ${systemViewsResponse.statusText}`);
|
|
645
|
+
if (!personalViewsResponse.ok) throw new Error(`Failed to fetch personal views: ${personalViewsResponse.statusText}`);
|
|
646
|
+
const systemViews = await systemViewsResponse.json();
|
|
647
|
+
const personalViews = await personalViewsResponse.json();
|
|
648
|
+
const views = [
|
|
649
|
+
...systemViews.value.map((v) => ({
|
|
650
|
+
id: v.savedqueryid,
|
|
651
|
+
name: v.name,
|
|
652
|
+
fetchxml: v.fetchxml,
|
|
653
|
+
layoutxml: v.layoutxml,
|
|
654
|
+
isDefault: v.isdefault || false,
|
|
655
|
+
isPersonal: false,
|
|
656
|
+
returnedtypecode: v.returnedtypecode,
|
|
657
|
+
description: v.description
|
|
658
|
+
})),
|
|
659
|
+
...personalViews.value.map((v) => ({
|
|
660
|
+
id: v.userqueryid,
|
|
661
|
+
name: v.name,
|
|
662
|
+
fetchxml: v.fetchxml,
|
|
663
|
+
layoutxml: v.layoutxml,
|
|
664
|
+
isDefault: false,
|
|
665
|
+
isPersonal: true,
|
|
666
|
+
returnedtypecode: v.returnedtypecode,
|
|
667
|
+
description: v.description
|
|
668
|
+
}))
|
|
669
|
+
];
|
|
670
|
+
views.sort((a, b) => {
|
|
671
|
+
if (a.isDefault && !b.isDefault) return -1;
|
|
672
|
+
if (!a.isDefault && b.isDefault) return 1;
|
|
673
|
+
return a.name.localeCompare(b.name);
|
|
674
|
+
});
|
|
675
|
+
return views;
|
|
676
|
+
}
|
|
677
|
+
async updateView(isPersonal, viewId, data) {
|
|
678
|
+
const entityName = isPersonal ? "userquery" : "savedquery";
|
|
679
|
+
await this.xrm.WebApi.updateRecord(entityName, viewId.replace(/[{}]/g, ""), data);
|
|
680
|
+
}
|
|
681
|
+
async createView(entityLogicalName, data) {
|
|
682
|
+
const payload = {
|
|
683
|
+
name: data.name,
|
|
684
|
+
returnedtypecode: entityLogicalName,
|
|
685
|
+
fetchxml: data.fetchxml,
|
|
686
|
+
layoutxml: data.layoutxml,
|
|
687
|
+
querytype: 0,
|
|
688
|
+
statecode: 0,
|
|
689
|
+
...data.description && { description: data.description }
|
|
690
|
+
};
|
|
691
|
+
const result = await this.xrm.WebApi.createRecord("userquery", payload);
|
|
692
|
+
return { id: result.id };
|
|
693
|
+
}
|
|
694
|
+
async deleteView(isPersonal, viewId) {
|
|
695
|
+
const entityName = isPersonal ? "userquery" : "savedquery";
|
|
696
|
+
await this.xrm.WebApi.deleteRecord(entityName, viewId.replace(/[{}]/g, ""));
|
|
697
|
+
}
|
|
698
|
+
async publishEntity(entityLogicalName) {
|
|
699
|
+
const clientUrl = this.getClientUrl();
|
|
700
|
+
const url = `${clientUrl}/api/data/v9.2/PublishXml`;
|
|
701
|
+
const parameterXml = `<importexportxml><entities><entity>${entityLogicalName}</entity></entities></importexportxml>`;
|
|
702
|
+
const response = await fetch(url, {
|
|
703
|
+
method: "POST",
|
|
704
|
+
headers: { "Accept": "application/json", "Content-Type": "application/json; charset=utf-8", "OData-MaxVersion": "4.0", "OData-Version": "4.0" },
|
|
705
|
+
body: JSON.stringify({ ParameterXml: parameterXml })
|
|
706
|
+
});
|
|
707
|
+
if (!response.ok) throw new Error(`Failed to publish entity: ${response.statusText}`);
|
|
708
|
+
}
|
|
709
|
+
// Global Option Set Operations
|
|
710
|
+
async getGlobalOptionSets() {
|
|
711
|
+
const clientUrl = this.getClientUrl();
|
|
712
|
+
const url = `${clientUrl}/api/data/v9.2/GlobalOptionSetDefinitions`;
|
|
713
|
+
const response = await fetch(url, {
|
|
714
|
+
method: "GET",
|
|
715
|
+
headers: { "Accept": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0" }
|
|
716
|
+
});
|
|
717
|
+
if (!response.ok) throw new Error(`Failed to fetch global option sets: ${response.statusText}`);
|
|
718
|
+
const result = await response.json();
|
|
719
|
+
return result.value.filter((os) => os.OptionSetType === "Picklist" && os.IsGlobal);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
function generateId() {
|
|
723
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
724
|
+
const r = Math.random() * 16 | 0;
|
|
725
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
726
|
+
return v.toString(16);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
var MockClient = class {
|
|
730
|
+
constructor(config, logger) {
|
|
731
|
+
this.dataStore = config?.records ?? {};
|
|
732
|
+
this.entityDefs = config?.entityDefinitions ?? [];
|
|
733
|
+
this.attrMeta = config?.attributeMetadata ?? {};
|
|
734
|
+
this.viewStore = config?.views ?? {};
|
|
735
|
+
this.relMeta = config?.relationshipMetadata ?? {};
|
|
736
|
+
this.globalOptionSets = config?.globalOptionSets ?? [];
|
|
737
|
+
this.delayMs = config?.delayMs ?? 0;
|
|
738
|
+
this.log = logger ?? console;
|
|
739
|
+
}
|
|
740
|
+
async delay() {
|
|
741
|
+
if (this.delayMs > 0) {
|
|
742
|
+
return new Promise((resolve) => setTimeout(resolve, this.delayMs));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// CRUD Operations
|
|
746
|
+
async retrieve(entitySetName, id) {
|
|
747
|
+
await this.delay();
|
|
748
|
+
const records = this.dataStore[entitySetName] ?? [];
|
|
749
|
+
const pkField = entitySetName.slice(0, -1) + "id";
|
|
750
|
+
const record = records.find((r) => r[pkField] === id || r.id === id);
|
|
751
|
+
if (!record) throw new Error(`Record ${id} not found in ${entitySetName}`);
|
|
752
|
+
return record;
|
|
753
|
+
}
|
|
754
|
+
async retrieveMultiple(entitySetName) {
|
|
755
|
+
await this.delay();
|
|
756
|
+
return { value: this.dataStore[entitySetName] ?? [] };
|
|
757
|
+
}
|
|
758
|
+
async create(entitySetName, data) {
|
|
759
|
+
await this.delay();
|
|
760
|
+
const id = generateId();
|
|
761
|
+
const pkField = entitySetName.slice(0, -1) + "id";
|
|
762
|
+
const record = { ...data, [pkField]: id };
|
|
763
|
+
if (!this.dataStore[entitySetName]) this.dataStore[entitySetName] = [];
|
|
764
|
+
this.dataStore[entitySetName].push(record);
|
|
765
|
+
return { id };
|
|
766
|
+
}
|
|
767
|
+
async update(entitySetName, id, data) {
|
|
768
|
+
await this.delay();
|
|
769
|
+
const records = this.dataStore[entitySetName] ?? [];
|
|
770
|
+
const pkField = entitySetName.slice(0, -1) + "id";
|
|
771
|
+
const index = records.findIndex((r) => r[pkField] === id || r.id === id);
|
|
772
|
+
if (index === -1) throw new Error(`Record ${id} not found in ${entitySetName}`);
|
|
773
|
+
this.dataStore[entitySetName][index] = { ...records[index], ...data };
|
|
774
|
+
}
|
|
775
|
+
async delete(entitySetName, id) {
|
|
776
|
+
await this.delay();
|
|
777
|
+
const records = this.dataStore[entitySetName] ?? [];
|
|
778
|
+
const pkField = entitySetName.slice(0, -1) + "id";
|
|
779
|
+
const index = records.findIndex((r) => r[pkField] === id || r.id === id);
|
|
780
|
+
if (index === -1) throw new Error(`Record ${id} not found in ${entitySetName}`);
|
|
781
|
+
this.dataStore[entitySetName].splice(index, 1);
|
|
782
|
+
}
|
|
783
|
+
// Batch Operations
|
|
784
|
+
async batchDelete(entitySetName, ids) {
|
|
785
|
+
const succeeded = [];
|
|
786
|
+
const failed = [];
|
|
787
|
+
for (const id of ids) {
|
|
788
|
+
try {
|
|
789
|
+
await this.delete(entitySetName, id);
|
|
790
|
+
succeeded.push(id);
|
|
791
|
+
} catch (err) {
|
|
792
|
+
failed.push({ id, error: err instanceof Error ? err.message : "Delete failed" });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return { succeeded, failed };
|
|
796
|
+
}
|
|
797
|
+
async batchUpdate(entitySetName, records) {
|
|
798
|
+
const succeeded = [];
|
|
799
|
+
const failed = [];
|
|
800
|
+
for (const record of records) {
|
|
801
|
+
try {
|
|
802
|
+
await this.update(entitySetName, record.id, record.data);
|
|
803
|
+
succeeded.push(record.id);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
failed.push({ id: record.id, error: err instanceof Error ? err.message : "Update failed" });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return { succeeded, failed };
|
|
809
|
+
}
|
|
810
|
+
async batchCreate(entitySetName, records) {
|
|
811
|
+
const succeeded = [];
|
|
812
|
+
const failed = [];
|
|
813
|
+
for (let i = 0; i < records.length; i++) {
|
|
814
|
+
try {
|
|
815
|
+
const result = await this.create(entitySetName, records[i]);
|
|
816
|
+
succeeded.push({ id: result.id, data: records[i] });
|
|
817
|
+
} catch (err) {
|
|
818
|
+
failed.push({ rowNumber: i + 1, error: err instanceof Error ? err.message : "Create failed" });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return { succeeded, failed };
|
|
822
|
+
}
|
|
823
|
+
// Metadata Operations
|
|
824
|
+
async getEntityDefinitions(_filter) {
|
|
825
|
+
await this.delay();
|
|
826
|
+
return this.entityDefs;
|
|
827
|
+
}
|
|
828
|
+
async getAttributeMetadata(entityLogicalName) {
|
|
829
|
+
await this.delay();
|
|
830
|
+
return this.attrMeta[entityLogicalName] ?? [];
|
|
831
|
+
}
|
|
832
|
+
async getRelationshipMetadata(entityLogicalName) {
|
|
833
|
+
await this.delay();
|
|
834
|
+
return this.relMeta[entityLogicalName] ?? [];
|
|
835
|
+
}
|
|
836
|
+
// View Operations
|
|
837
|
+
async getViews(entityLogicalName) {
|
|
838
|
+
await this.delay();
|
|
839
|
+
return this.viewStore[entityLogicalName] ?? [];
|
|
840
|
+
}
|
|
841
|
+
async updateView(_isPersonal, viewId, data) {
|
|
842
|
+
await this.delay();
|
|
843
|
+
for (const entity of Object.keys(this.viewStore)) {
|
|
844
|
+
const views = this.viewStore[entity];
|
|
845
|
+
const idx = views.findIndex((v) => v.id === viewId);
|
|
846
|
+
if (idx !== -1) {
|
|
847
|
+
this.viewStore[entity][idx] = { ...views[idx], ...data };
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
throw new Error(`View ${viewId} not found`);
|
|
852
|
+
}
|
|
853
|
+
async createView(entityLogicalName, data) {
|
|
854
|
+
await this.delay();
|
|
855
|
+
const id = generateId();
|
|
856
|
+
const view = {
|
|
857
|
+
id,
|
|
858
|
+
name: data.name,
|
|
859
|
+
fetchxml: data.fetchxml,
|
|
860
|
+
layoutxml: data.layoutxml,
|
|
861
|
+
isDefault: false,
|
|
862
|
+
isPersonal: true,
|
|
863
|
+
returnedtypecode: entityLogicalName,
|
|
864
|
+
description: data.description
|
|
865
|
+
};
|
|
866
|
+
if (!this.viewStore[entityLogicalName]) this.viewStore[entityLogicalName] = [];
|
|
867
|
+
this.viewStore[entityLogicalName].push(view);
|
|
868
|
+
return { id };
|
|
869
|
+
}
|
|
870
|
+
async deleteView(_isPersonal, viewId) {
|
|
871
|
+
await this.delay();
|
|
872
|
+
for (const entity of Object.keys(this.viewStore)) {
|
|
873
|
+
const views = this.viewStore[entity];
|
|
874
|
+
const idx = views.findIndex((v) => v.id === viewId);
|
|
875
|
+
if (idx !== -1) {
|
|
876
|
+
this.viewStore[entity].splice(idx, 1);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
throw new Error(`View ${viewId} not found`);
|
|
881
|
+
}
|
|
882
|
+
async publishEntity() {
|
|
883
|
+
await this.delay();
|
|
884
|
+
this.log.log("[MockClient] publishEntity called (no-op in mock mode)");
|
|
885
|
+
}
|
|
886
|
+
// Global Option Set Operations
|
|
887
|
+
async getGlobalOptionSets() {
|
|
888
|
+
await this.delay();
|
|
889
|
+
return this.globalOptionSets;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
function createClient2(options) {
|
|
893
|
+
const logger = options?.logger;
|
|
894
|
+
if (options?.baseUrl && options?.token) {
|
|
895
|
+
return new FetchClient(options.baseUrl, options.token, logger);
|
|
896
|
+
}
|
|
897
|
+
if (options?.xrm) {
|
|
898
|
+
return new XrmClient(options.xrm, logger);
|
|
899
|
+
}
|
|
900
|
+
if (typeof window !== "undefined") {
|
|
901
|
+
const win = window;
|
|
902
|
+
if (win.__DYNAMICS_URL && win.__DYNAMICS_TOKEN) {
|
|
903
|
+
return new FetchClient(win.__DYNAMICS_URL, win.__DYNAMICS_TOKEN, logger);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.Xrm !== "undefined") {
|
|
907
|
+
return new XrmClient(globalThis.Xrm, logger);
|
|
908
|
+
}
|
|
909
|
+
return new MockClient(options?.mockData, logger);
|
|
910
|
+
}
|
|
911
|
+
function getEnvironmentInfo() {
|
|
912
|
+
const hostname = typeof window !== "undefined" ? window.location?.hostname ?? "unknown" : "node";
|
|
913
|
+
const hasXrm = typeof globalThis !== "undefined" && typeof globalThis.Xrm !== "undefined";
|
|
914
|
+
const win = typeof window !== "undefined" ? window : {};
|
|
915
|
+
const hasToken = !!(win.__DYNAMICS_URL && win.__DYNAMICS_TOKEN);
|
|
916
|
+
let type = "mock";
|
|
917
|
+
if (hasToken) type = "fetch";
|
|
918
|
+
else if (hasXrm) type = "xrm";
|
|
919
|
+
return { type, hostname, baseUrl: win.__DYNAMICS_URL, hasToken, hasXrm };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// src/index.ts
|
|
925
|
+
init_esm_shims();
|
|
926
|
+
|
|
927
|
+
// src/metadata/adapter.ts
|
|
928
|
+
init_esm_shims();
|
|
929
|
+
function toAttributeMetadataInput(attr) {
|
|
930
|
+
return {
|
|
931
|
+
LogicalName: attr.LogicalName,
|
|
932
|
+
SchemaName: attr.SchemaName,
|
|
933
|
+
DisplayName: attr.DisplayName,
|
|
934
|
+
AttributeType: attr.AttributeType,
|
|
935
|
+
RequiredLevel: attr.RequiredLevel,
|
|
936
|
+
IsValidForRead: attr.IsValidForRead,
|
|
937
|
+
IsValidForCreate: attr.IsValidForCreate,
|
|
938
|
+
IsValidForUpdate: attr.IsValidForUpdate,
|
|
939
|
+
IsPrimaryId: attr.IsPrimaryId,
|
|
940
|
+
IsPrimaryName: attr.IsPrimaryName,
|
|
941
|
+
MaxLength: attr.MaxLength,
|
|
942
|
+
Precision: attr.Precision,
|
|
943
|
+
MinValue: attr.MinValue,
|
|
944
|
+
MaxValue: attr.MaxValue,
|
|
945
|
+
Format: attr.Format,
|
|
946
|
+
FormatName: attr.FormatName,
|
|
947
|
+
OptionSet: attr.OptionSet,
|
|
948
|
+
GlobalOptionSet: attr.GlobalOptionSet,
|
|
949
|
+
DateTimeBehavior: attr.DateTimeBehavior,
|
|
950
|
+
Targets: attr.Targets
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function toEntityMetadataInput(entity, attributes) {
|
|
954
|
+
if (!entity?.LogicalName) {
|
|
955
|
+
throw new Error("toEntityMetadataInput: entity.LogicalName is required.");
|
|
956
|
+
}
|
|
957
|
+
if (!entity.EntitySetName) {
|
|
958
|
+
throw new Error(`toEntityMetadataInput: ${entity.LogicalName} is missing EntitySetName.`);
|
|
959
|
+
}
|
|
960
|
+
if (!entity.PrimaryIdAttribute) {
|
|
961
|
+
throw new Error(`toEntityMetadataInput: ${entity.LogicalName} is missing PrimaryIdAttribute.`);
|
|
962
|
+
}
|
|
963
|
+
const primaryName = entity.PrimaryNameAttribute || entity.PrimaryIdAttribute;
|
|
964
|
+
return {
|
|
965
|
+
LogicalName: entity.LogicalName,
|
|
966
|
+
EntitySetName: entity.EntitySetName,
|
|
967
|
+
SchemaName: entity.SchemaName,
|
|
968
|
+
DisplayName: entity.DisplayName,
|
|
969
|
+
DisplayCollectionName: entity.DisplayCollectionName,
|
|
970
|
+
PrimaryIdAttribute: entity.PrimaryIdAttribute,
|
|
971
|
+
PrimaryNameAttribute: primaryName,
|
|
972
|
+
IsCustomEntity: entity.IsCustomEntity,
|
|
973
|
+
Attributes: attributes.map(toAttributeMetadataInput)
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/metadata/fetch.ts
|
|
978
|
+
init_esm_shims();
|
|
979
|
+
|
|
980
|
+
// src/metadata/typedCast.ts
|
|
981
|
+
init_esm_shims();
|
|
982
|
+
|
|
983
|
+
// src/metadata/http.ts
|
|
984
|
+
init_esm_shims();
|
|
985
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
986
|
+
function buildUrl(http, relativePath) {
|
|
987
|
+
const version = http.apiVersion ?? "9.2";
|
|
988
|
+
const base = http.baseUrl.replace(/\/+$/, "");
|
|
989
|
+
return `${base}/api/data/v${version}/${relativePath}`;
|
|
990
|
+
}
|
|
991
|
+
async function getWithRetry(http, url, maxRetries) {
|
|
992
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
993
|
+
const res = await fetch(url, {
|
|
994
|
+
headers: {
|
|
995
|
+
Authorization: `Bearer ${http.token}`,
|
|
996
|
+
Accept: "application/json",
|
|
997
|
+
"OData-MaxVersion": "4.0",
|
|
998
|
+
"OData-Version": "4.0"
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
if (res.status === 429 || res.status === 503) {
|
|
1002
|
+
if (attempt >= maxRetries) {
|
|
1003
|
+
throw new Error(`Dataverse throttled (${res.status}) after ${attempt} retries: ${url}`);
|
|
1004
|
+
}
|
|
1005
|
+
const headerWait = Number(res.headers.get("Retry-After"));
|
|
1006
|
+
const waitSec = Number.isFinite(headerWait) && headerWait > 0 ? headerWait : Math.min(2 ** attempt, 30);
|
|
1007
|
+
await sleep(waitSec * 1e3);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (!res.ok) {
|
|
1011
|
+
const body = await res.text().catch(() => "");
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`Dataverse metadata GET ${res.status} ${res.statusText}: ${url}
|
|
1014
|
+
${body.slice(0, 300)}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return await res.json();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function metadataGet(http, relativePath, maxRetries = 4) {
|
|
1021
|
+
return getWithRetry(http, buildUrl(http, relativePath), maxRetries);
|
|
1022
|
+
}
|
|
1023
|
+
async function metadataGetAll(http, relativePath, maxRetries = 4) {
|
|
1024
|
+
const out = [];
|
|
1025
|
+
let url = buildUrl(http, relativePath);
|
|
1026
|
+
while (url) {
|
|
1027
|
+
const page = await getWithRetry(http, url, maxRetries);
|
|
1028
|
+
if (page.value) out.push(...page.value);
|
|
1029
|
+
url = page["@odata.nextLink"];
|
|
1030
|
+
}
|
|
1031
|
+
return out;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/metadata/typedCast.ts
|
|
1035
|
+
var SPECS = [
|
|
1036
|
+
{ types: ["String"], cast: "StringAttributeMetadata", select: ["MaxLength", "Format"] },
|
|
1037
|
+
{ types: ["Memo"], cast: "MemoAttributeMetadata", select: ["MaxLength", "Format"] },
|
|
1038
|
+
{ types: ["Integer"], cast: "IntegerAttributeMetadata", select: ["MinValue", "MaxValue", "Format"] },
|
|
1039
|
+
{ types: ["BigInt"], cast: "BigIntAttributeMetadata", select: ["MinValue", "MaxValue"] },
|
|
1040
|
+
{ types: ["Decimal"], cast: "DecimalAttributeMetadata", select: ["MinValue", "MaxValue", "Precision"] },
|
|
1041
|
+
{ types: ["Double"], cast: "DoubleAttributeMetadata", select: ["MinValue", "MaxValue", "Precision"] },
|
|
1042
|
+
{ types: ["Money"], cast: "MoneyAttributeMetadata", select: ["MinValue", "MaxValue", "Precision"] },
|
|
1043
|
+
{ types: ["DateTime"], cast: "DateTimeAttributeMetadata", select: ["Format", "DateTimeBehavior"] }
|
|
1044
|
+
];
|
|
1045
|
+
function mergeScalarDetail(attrs, details) {
|
|
1046
|
+
const byName = new Map(details.map((d) => [d.LogicalName, d]));
|
|
1047
|
+
for (const a of attrs) {
|
|
1048
|
+
const d = byName.get(a.LogicalName);
|
|
1049
|
+
if (!d) continue;
|
|
1050
|
+
if (d.MaxLength != null && a.MaxLength == null) a.MaxLength = d.MaxLength;
|
|
1051
|
+
if (d.MinValue != null && a.MinValue == null) a.MinValue = d.MinValue;
|
|
1052
|
+
if (d.MaxValue != null && a.MaxValue == null) a.MaxValue = d.MaxValue;
|
|
1053
|
+
if (d.Precision != null && a.Precision == null) a.Precision = d.Precision;
|
|
1054
|
+
if (d.Format != null && a.Format == null) a.Format = d.Format;
|
|
1055
|
+
if (d.DateTimeBehavior != null && a.DateTimeBehavior == null) a.DateTimeBehavior = d.DateTimeBehavior;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function augmentScalarDetail(http, logicalName, attrs, get = async (h, p) => ({ value: await metadataGetAll(h, p) })) {
|
|
1059
|
+
const presentTypes = new Set(attrs.map((a) => a.AttributeType));
|
|
1060
|
+
for (const spec of SPECS) {
|
|
1061
|
+
if (!spec.types.some((t) => presentTypes.has(t))) continue;
|
|
1062
|
+
const select = ["LogicalName", ...spec.select].join(",");
|
|
1063
|
+
const path2 = `EntityDefinitions(LogicalName='${logicalName}')/Attributes/Microsoft.Dynamics.CRM.${spec.cast}?$select=${select}`;
|
|
1064
|
+
try {
|
|
1065
|
+
const resp = await get(http, path2);
|
|
1066
|
+
mergeScalarDetail(attrs, resp.value ?? []);
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/validate.ts
|
|
1073
|
+
init_esm_shims();
|
|
1074
|
+
var LOGICAL_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
1075
|
+
function assertValidEntityName(name) {
|
|
1076
|
+
if (!name || !LOGICAL_NAME_PATTERN.test(name)) {
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`Invalid entity logical name ${JSON.stringify(name)} \u2014 expected a lowercase letter followed by [a-z0-9_] (Dataverse logical-name rule).`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return name;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/metadata/fetch.ts
|
|
1085
|
+
async function fetchEntityMetadata(client, logicalName, options = {}) {
|
|
1086
|
+
assertValidEntityName(logicalName);
|
|
1087
|
+
const defs = await client.getEntityDefinitions(`LogicalName eq '${logicalName}'`);
|
|
1088
|
+
const entity = defs[0];
|
|
1089
|
+
if (!entity) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`Entity '${logicalName}' was not found in this org (no EntityDefinitions match). Check the logical name (singular, lowercase) and that your token targets the right org.`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
const attributes = await client.getAttributeMetadata(logicalName, options.attributeFilter);
|
|
1095
|
+
const input = toEntityMetadataInput(entity, attributes);
|
|
1096
|
+
if (options.augment) {
|
|
1097
|
+
try {
|
|
1098
|
+
await augmentScalarDetail(options.augment, logicalName, input.Attributes);
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
console.warn(
|
|
1101
|
+
`[dvgen] scalar detail augmentation skipped for ${logicalName}: ${e instanceof Error ? e.message : String(e)}`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return input;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/io/write.ts
|
|
1109
|
+
init_esm_shims();
|
|
1110
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
1111
|
+
import { dirname } from "path";
|
|
1112
|
+
function safeWrite(path2, content, opts = {}) {
|
|
1113
|
+
const exists = existsSync(path2);
|
|
1114
|
+
const current = exists ? readFileSync(path2, "utf8") : void 0;
|
|
1115
|
+
if (exists && current === content) return "unchanged";
|
|
1116
|
+
if (opts.diff && exists) printDiff(path2, current ?? "", content);
|
|
1117
|
+
if (opts.dryRun) return "dry-run";
|
|
1118
|
+
if (exists && !opts.force) {
|
|
1119
|
+
return "skipped-exists";
|
|
1120
|
+
}
|
|
1121
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
1122
|
+
writeFileSync(path2, content, "utf8");
|
|
1123
|
+
return exists ? "overwritten" : "created";
|
|
1124
|
+
}
|
|
1125
|
+
function printDiff(path2, before, after) {
|
|
1126
|
+
const a = before.split("\n");
|
|
1127
|
+
const b = after.split("\n");
|
|
1128
|
+
console.log(`
|
|
1129
|
+
--- ${path2} (existing)
|
|
1130
|
+
+++ ${path2} (generated)`);
|
|
1131
|
+
const max = Math.max(a.length, b.length);
|
|
1132
|
+
for (let i = 0; i < max; i += 1) {
|
|
1133
|
+
if (a[i] === b[i]) continue;
|
|
1134
|
+
if (a[i] !== void 0) console.log(`- ${a[i]}`);
|
|
1135
|
+
if (b[i] !== void 0) console.log(`+ ${b[i]}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/connection.ts
|
|
1140
|
+
init_esm_shims();
|
|
1141
|
+
var import_api_client = __toESM(require_dist(), 1);
|
|
1142
|
+
|
|
1143
|
+
// ../dev-tools/dist/index.mjs
|
|
1144
|
+
init_esm_shims();
|
|
1145
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1146
|
+
import { execFileSync } from "child_process";
|
|
1147
|
+
var URL_ENV_KEYS = [
|
|
1148
|
+
"VITE_DYNAMICS_URL",
|
|
1149
|
+
"REACT_APP_DYNAMICS_URL",
|
|
1150
|
+
"DYNAMICS_URL"
|
|
1151
|
+
];
|
|
1152
|
+
function parseEnvFile(content) {
|
|
1153
|
+
const result = {};
|
|
1154
|
+
if (content.charCodeAt(0) === 65279) {
|
|
1155
|
+
content = content.slice(1);
|
|
1156
|
+
}
|
|
1157
|
+
const lines = content.split(/\r?\n/);
|
|
1158
|
+
for (const line of lines) {
|
|
1159
|
+
const trimmed = line.trim();
|
|
1160
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1161
|
+
const eq = trimmed.indexOf("=");
|
|
1162
|
+
if (eq === -1) continue;
|
|
1163
|
+
const key = trimmed.slice(0, eq).trim();
|
|
1164
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
1165
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1166
|
+
value = value.slice(1, -1);
|
|
1167
|
+
}
|
|
1168
|
+
result[key] = value;
|
|
1169
|
+
}
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
function loadEnv(options = {}) {
|
|
1173
|
+
let values = {};
|
|
1174
|
+
if (options.path) {
|
|
1175
|
+
if (!existsSync2(options.path)) {
|
|
1176
|
+
throw new Error(`Env file not found: ${options.path}`);
|
|
1177
|
+
}
|
|
1178
|
+
const content = readFileSync2(options.path, "utf-8");
|
|
1179
|
+
values = parseEnvFile(content);
|
|
1180
|
+
}
|
|
1181
|
+
const fallback = options.fallbackToProcessEnv ?? !options.path;
|
|
1182
|
+
const lookup = (key) => {
|
|
1183
|
+
if (key in values && values[key] !== "") return values[key];
|
|
1184
|
+
if (fallback) {
|
|
1185
|
+
const v = process.env[key];
|
|
1186
|
+
if (v !== void 0 && v !== "") return v;
|
|
1187
|
+
}
|
|
1188
|
+
return void 0;
|
|
1189
|
+
};
|
|
1190
|
+
for (const key of URL_ENV_KEYS) {
|
|
1191
|
+
const v = lookup(key);
|
|
1192
|
+
if (v !== void 0) {
|
|
1193
|
+
return { url: v, matchedKey: key, values };
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return { url: void 0, matchedKey: void 0, values };
|
|
1197
|
+
}
|
|
1198
|
+
var AzCliError = class extends Error {
|
|
1199
|
+
cause;
|
|
1200
|
+
constructor(message, cause) {
|
|
1201
|
+
super(message);
|
|
1202
|
+
this.name = "AzCliError";
|
|
1203
|
+
if (cause !== void 0) this.cause = cause;
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
function getToken(options) {
|
|
1207
|
+
const { resource } = options;
|
|
1208
|
+
if (!resource) {
|
|
1209
|
+
throw new Error("getToken: resource is required (Dataverse URL)");
|
|
1210
|
+
}
|
|
1211
|
+
const az = options.azCommand ?? "az";
|
|
1212
|
+
const args = [
|
|
1213
|
+
"account",
|
|
1214
|
+
"get-access-token",
|
|
1215
|
+
"--resource",
|
|
1216
|
+
resource,
|
|
1217
|
+
"--output",
|
|
1218
|
+
"json"
|
|
1219
|
+
];
|
|
1220
|
+
let stdout;
|
|
1221
|
+
try {
|
|
1222
|
+
if (options.exec) {
|
|
1223
|
+
stdout = options.exec(az, args);
|
|
1224
|
+
} else {
|
|
1225
|
+
stdout = execFileSync(az, args, {
|
|
1226
|
+
encoding: "utf-8",
|
|
1227
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
const errObj = err;
|
|
1232
|
+
const stderr = typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr?.toString("utf-8") ?? errObj.message ?? "";
|
|
1233
|
+
if (errObj.code === "ENOENT") {
|
|
1234
|
+
throw new AzCliError(
|
|
1235
|
+
"Azure CLI (`az`) is not installed or not on PATH. Install: https://learn.microsoft.com/cli/azure/install-azure-cli",
|
|
1236
|
+
err
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (/please run.*az login/i.test(stderr) || /not logged in/i.test(stderr)) {
|
|
1240
|
+
throw new AzCliError(
|
|
1241
|
+
"Azure CLI is not authenticated. Run `az login` and retry.",
|
|
1242
|
+
err
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
throw new AzCliError(
|
|
1246
|
+
`Failed to acquire token via Azure CLI: ${stderr.trim() || "unknown error"}`,
|
|
1247
|
+
err
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
let parsed;
|
|
1251
|
+
try {
|
|
1252
|
+
parsed = JSON.parse(stdout);
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
throw new AzCliError(`Could not parse Azure CLI JSON output: ${stdout}`, err);
|
|
1255
|
+
}
|
|
1256
|
+
if (!parsed.accessToken) {
|
|
1257
|
+
throw new AzCliError(`Azure CLI returned no accessToken: ${stdout}`);
|
|
1258
|
+
}
|
|
1259
|
+
const expiresOn = new Date(parsed.expiresOn);
|
|
1260
|
+
if (Number.isNaN(expiresOn.getTime())) {
|
|
1261
|
+
throw new AzCliError(`Azure CLI returned unparseable expiresOn: ${parsed.expiresOn}`);
|
|
1262
|
+
}
|
|
1263
|
+
return { token: parsed.accessToken, expiresOn };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/connection.ts
|
|
1267
|
+
function resolveConnection(opts) {
|
|
1268
|
+
const env = loadEnv({ path: opts.envFile, fallbackToProcessEnv: true });
|
|
1269
|
+
const url = opts.url ?? env.url;
|
|
1270
|
+
if (!url) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
"No Dataverse URL found. Pass --url <url>, or set one of VITE_DYNAMICS_URL / REACT_APP_DYNAMICS_URL / DYNAMICS_URL (optionally via --env-file)."
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
let token = opts.token;
|
|
1276
|
+
let tokenSource = "flag";
|
|
1277
|
+
if (!token) {
|
|
1278
|
+
token = env.values.DYNAMICS_TOKEN ?? env.values.VITE_DYNAMICS_TOKEN ?? env.values.REACT_APP_DYNAMICS_TOKEN ?? process.env.DYNAMICS_TOKEN ?? process.env.VITE_DYNAMICS_TOKEN ?? process.env.REACT_APP_DYNAMICS_TOKEN;
|
|
1279
|
+
if (token) tokenSource = "env";
|
|
1280
|
+
}
|
|
1281
|
+
if (!token) {
|
|
1282
|
+
token = getToken({ resource: url }).token;
|
|
1283
|
+
tokenSource = "azure-cli";
|
|
1284
|
+
}
|
|
1285
|
+
const client = (0, import_api_client.createClient)({ baseUrl: url, token });
|
|
1286
|
+
return { client, url, token, tokenSource };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/metadata/links.ts
|
|
1290
|
+
init_esm_shims();
|
|
1291
|
+
async function deriveParentLookupLinks(client, entity) {
|
|
1292
|
+
try {
|
|
1293
|
+
const lookupAttrs = new Set(
|
|
1294
|
+
entity.Attributes.filter(
|
|
1295
|
+
(a) => ["Lookup", "Customer", "Owner"].includes(a.AttributeType)
|
|
1296
|
+
).map((a) => a.LogicalName)
|
|
1297
|
+
);
|
|
1298
|
+
const rels = await client.getRelationshipMetadata(entity.LogicalName);
|
|
1299
|
+
const seenAliases = /* @__PURE__ */ new Set();
|
|
1300
|
+
return rels.filter(
|
|
1301
|
+
(r) => r.ReferencingEntity === entity.LogicalName && lookupAttrs.has(r.ReferencingAttribute)
|
|
1302
|
+
).map((r) => {
|
|
1303
|
+
let alias = r.ReferencingEntityNavigationPropertyName || r.ReferencedEntity;
|
|
1304
|
+
let n = 2;
|
|
1305
|
+
while (seenAliases.has(alias)) alias = `${r.ReferencedEntity}${n++}`;
|
|
1306
|
+
seenAliases.add(alias);
|
|
1307
|
+
return {
|
|
1308
|
+
entity: r.ReferencedEntity,
|
|
1309
|
+
from: r.ReferencedAttribute,
|
|
1310
|
+
to: r.ReferencingAttribute,
|
|
1311
|
+
alias
|
|
1312
|
+
};
|
|
1313
|
+
});
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
console.warn(
|
|
1316
|
+
` \u26A0 could not fetch relationships for ${entity.LogicalName}: ${e instanceof Error ? e.message : String(e)} (emitting TODO scaffold)`
|
|
1317
|
+
);
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/design/parse.ts
|
|
1323
|
+
init_esm_shims();
|
|
1324
|
+
var FORM_DESIGN_KIND = "dynamics-ui-kit.form-design";
|
|
1325
|
+
function parseDesignFile(raw) {
|
|
1326
|
+
let parsed;
|
|
1327
|
+
try {
|
|
1328
|
+
parsed = JSON.parse(raw);
|
|
1329
|
+
} catch (e) {
|
|
1330
|
+
throw new Error(`Design file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
1331
|
+
}
|
|
1332
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1333
|
+
throw new Error("Design file must be a JSON object.");
|
|
1334
|
+
}
|
|
1335
|
+
const file = parsed;
|
|
1336
|
+
if (file.kind && file.kind !== FORM_DESIGN_KIND) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`Unexpected design kind ${JSON.stringify(file.kind)} (expected ${JSON.stringify(FORM_DESIGN_KIND)}). Is this a *.design.json exported from the form designer?`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (!Array.isArray(file.forms)) {
|
|
1342
|
+
throw new Error("Design file has no `forms` array \u2014 nothing to scan.");
|
|
1343
|
+
}
|
|
1344
|
+
return file;
|
|
1345
|
+
}
|
|
1346
|
+
function collectEntities(design) {
|
|
1347
|
+
const found = /* @__PURE__ */ new Map();
|
|
1348
|
+
const skipped = [];
|
|
1349
|
+
const add = (raw, source) => {
|
|
1350
|
+
if (typeof raw !== "string") return;
|
|
1351
|
+
const name = raw.trim().toLowerCase();
|
|
1352
|
+
if (!name) return;
|
|
1353
|
+
try {
|
|
1354
|
+
assertValidEntityName(name);
|
|
1355
|
+
} catch {
|
|
1356
|
+
if (!skipped.some((s) => s.name === name)) {
|
|
1357
|
+
skipped.push({ name, reason: "not a valid Dataverse logical name" });
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (!found.has(name)) found.set(name, /* @__PURE__ */ new Set());
|
|
1362
|
+
found.get(name).add(source);
|
|
1363
|
+
};
|
|
1364
|
+
const walkSections = (sections, formName) => {
|
|
1365
|
+
for (const section of sections ?? []) {
|
|
1366
|
+
for (const row of section.rows ?? []) {
|
|
1367
|
+
for (const cell of row.cells ?? []) {
|
|
1368
|
+
const c = cell?.control;
|
|
1369
|
+
if (!c) continue;
|
|
1370
|
+
if (c.type === "subgrid") {
|
|
1371
|
+
add(c.properties?.entityName, `subgrid in "${formName}"`);
|
|
1372
|
+
}
|
|
1373
|
+
if (c.type === "lookup" && Array.isArray(c.dataBinding?.lookupTargets)) {
|
|
1374
|
+
for (const t of c.dataBinding.lookupTargets) {
|
|
1375
|
+
add(t, `lookup in "${formName}"`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
for (const form of design.forms ?? []) {
|
|
1383
|
+
const formName = form.name ?? "(unnamed form)";
|
|
1384
|
+
add(form.settings?.dataSources?.[0]?.entityName, `form "${formName}" (target)`);
|
|
1385
|
+
walkSections(form.sections, formName);
|
|
1386
|
+
for (const tab of form.tabs ?? []) walkSections(tab.sections, formName);
|
|
1387
|
+
}
|
|
1388
|
+
const entities = [...found.entries()].map(([logicalName, sources]) => ({ logicalName, sources: [...sources] })).sort((a, b) => a.logicalName.localeCompare(b.logicalName));
|
|
1389
|
+
return { entities, skipped };
|
|
1390
|
+
}
|
|
1391
|
+
export {
|
|
1392
|
+
FORM_DESIGN_KIND,
|
|
1393
|
+
augmentScalarDetail,
|
|
1394
|
+
collectEntities,
|
|
1395
|
+
deriveParentLookupLinks,
|
|
1396
|
+
fetchEntityMetadata,
|
|
1397
|
+
mergeScalarDetail,
|
|
1398
|
+
metadataGet,
|
|
1399
|
+
metadataGetAll,
|
|
1400
|
+
parseDesignFile,
|
|
1401
|
+
resolveConnection,
|
|
1402
|
+
safeWrite,
|
|
1403
|
+
toAttributeMetadataInput,
|
|
1404
|
+
toEntityMetadataInput
|
|
1405
|
+
};
|
|
1406
|
+
//# sourceMappingURL=index.mjs.map
|