@slamb2k/dvx 1.0.7

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.
@@ -0,0 +1,1041 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var DvxError = class extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "DvxError";
8
+ }
9
+ };
10
+ var AuthError = class extends DvxError {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "AuthError";
14
+ }
15
+ };
16
+ var AuthProfileNotFoundError = class extends AuthError {
17
+ constructor(profileName) {
18
+ super(`Auth profile '${profileName}' not found`);
19
+ this.name = "AuthProfileNotFoundError";
20
+ }
21
+ };
22
+ var AuthProfileExistsError = class extends AuthError {
23
+ constructor(profileName) {
24
+ super(`Auth profile '${profileName}' already exists`);
25
+ this.name = "AuthProfileExistsError";
26
+ }
27
+ };
28
+ var TokenAcquisitionError = class extends AuthError {
29
+ constructor(message) {
30
+ super(`Failed to acquire token: ${message}`);
31
+ this.name = "TokenAcquisitionError";
32
+ }
33
+ };
34
+ var DataverseError = class extends DvxError {
35
+ statusCode;
36
+ errorCode;
37
+ retryAfterSeconds;
38
+ constructor(message, statusCode, errorCode, retryAfterSeconds) {
39
+ super(message);
40
+ this.name = "DataverseError";
41
+ this.statusCode = statusCode;
42
+ this.errorCode = errorCode;
43
+ this.retryAfterSeconds = retryAfterSeconds;
44
+ }
45
+ };
46
+ var EntityNotFoundError = class extends DataverseError {
47
+ constructor(entity) {
48
+ super(`Entity '${entity}' not found`, 404);
49
+ this.name = "EntityNotFoundError";
50
+ }
51
+ };
52
+ var RecordNotFoundError = class extends DataverseError {
53
+ constructor(entity, id) {
54
+ super(`Record '${id}' not found in '${entity}'`, 404);
55
+ this.name = "RecordNotFoundError";
56
+ }
57
+ };
58
+ var ValidationError = class extends DvxError {
59
+ constructor(message) {
60
+ super(message);
61
+ this.name = "ValidationError";
62
+ }
63
+ };
64
+ var FetchXmlValidationError = class extends DvxError {
65
+ constructor(message) {
66
+ super(message);
67
+ this.name = "FetchXmlValidationError";
68
+ }
69
+ };
70
+ var ActionError = class extends DvxError {
71
+ constructor(message, statusCode) {
72
+ super(message);
73
+ this.statusCode = statusCode;
74
+ this.name = "ActionError";
75
+ }
76
+ };
77
+ var ImpersonationPrivilegeError = class extends DataverseError {
78
+ constructor(message = "Caller does not have the Act on Behalf of Another User privilege (prvActOnBehalfOfAnotherUser)") {
79
+ super(message, 403);
80
+ this.name = "ImpersonationPrivilegeError";
81
+ }
82
+ };
83
+ var PkceFlowError = class extends AuthError {
84
+ constructor(message) {
85
+ super(message);
86
+ this.name = "PkceFlowError";
87
+ }
88
+ };
89
+
90
+ // src/utils/cli.ts
91
+ import * as clack from "@clack/prompts";
92
+ var uxState = { quiet: false, noColor: false };
93
+ function setUxOptions(opts) {
94
+ uxState = opts;
95
+ }
96
+ function isInteractive() {
97
+ return Boolean(process.stderr.isTTY) && !uxState.quiet;
98
+ }
99
+ function createSpinner() {
100
+ if (!isInteractive()) {
101
+ return { start() {
102
+ }, stop() {
103
+ }, message() {
104
+ }, error() {
105
+ } };
106
+ }
107
+ const s = clack.spinner();
108
+ return {
109
+ start(msg) {
110
+ s.start(msg);
111
+ },
112
+ stop(msg) {
113
+ s.stop(msg);
114
+ },
115
+ message(msg) {
116
+ s.message(msg);
117
+ },
118
+ error(msg) {
119
+ s.stop(msg);
120
+ }
121
+ };
122
+ }
123
+ function logSuccess(msg) {
124
+ if (isInteractive()) {
125
+ clack.log.success(msg);
126
+ } else {
127
+ process.stderr.write(`${msg}
128
+ `);
129
+ }
130
+ }
131
+ function logError(msg) {
132
+ if (isInteractive()) {
133
+ clack.log.error(msg);
134
+ } else {
135
+ process.stderr.write(`Error: ${msg}
136
+ `);
137
+ }
138
+ }
139
+ function logWarn(msg) {
140
+ if (!isInteractive()) return;
141
+ clack.log.warn(msg);
142
+ }
143
+ function logInfo(msg) {
144
+ if (isInteractive()) {
145
+ clack.log.info(msg);
146
+ } else {
147
+ process.stderr.write(`Hint: ${msg}
148
+ `);
149
+ }
150
+ }
151
+ function logStep(msg) {
152
+ if (!isInteractive()) return;
153
+ clack.log.step(msg);
154
+ }
155
+ function logDryRun(method, url, body) {
156
+ if (isInteractive()) {
157
+ clack.log.warn(`[DRY RUN] ${method} ${url}`);
158
+ if (body !== void 0) {
159
+ clack.log.info(`Body: ${JSON.stringify(body)}`);
160
+ }
161
+ } else {
162
+ process.stderr.write(`[DRY RUN] ${method} ${url}
163
+ `);
164
+ if (body !== void 0) {
165
+ process.stderr.write(`[DRY RUN] Body: ${JSON.stringify(body)}
166
+ `);
167
+ }
168
+ }
169
+ }
170
+ function logMutationSuccess(msg) {
171
+ if (!isInteractive()) return;
172
+ clack.log.success(msg);
173
+ }
174
+ async function promptConfirmClack(msg) {
175
+ if (!isInteractive()) return false;
176
+ const result = await clack.confirm({ message: msg });
177
+ if (clack.isCancel(result)) return false;
178
+ return result;
179
+ }
180
+ async function promptUrl(msg, placeholder) {
181
+ const url = await clack.text({
182
+ message: msg,
183
+ placeholder: placeholder ?? "https://org.crm.dynamics.com",
184
+ validate: (value) => {
185
+ if (!value) return "Please enter a valid URL";
186
+ try {
187
+ new URL(value);
188
+ } catch {
189
+ return "Please enter a valid URL";
190
+ }
191
+ }
192
+ });
193
+ if (clack.isCancel(url)) {
194
+ clack.cancel("Operation cancelled.");
195
+ process.exit(0);
196
+ }
197
+ return url;
198
+ }
199
+ function stripAnsi(str) {
200
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
201
+ }
202
+
203
+ // src/client/create-client.ts
204
+ import * as path2 from "path";
205
+
206
+ // src/auth/auth-manager.ts
207
+ import * as fs from "fs";
208
+ import * as path from "path";
209
+ import { ConfidentialClientApplication, PublicClientApplication } from "@azure/msal-node";
210
+
211
+ // src/auth/auth-profile.ts
212
+ import { z } from "zod";
213
+ var AuthProfileSchema = z.object({
214
+ name: z.string().min(1),
215
+ type: z.enum(["service-principal", "delegated"]),
216
+ environmentUrl: z.string().url(),
217
+ tenantId: z.string().uuid(),
218
+ clientId: z.string().uuid(),
219
+ clientSecret: z.string().optional(),
220
+ homeAccountId: z.string().optional()
221
+ });
222
+ var AuthConfigSchema = z.object({
223
+ activeProfile: z.string().optional(),
224
+ profiles: z.record(z.string(), AuthProfileSchema)
225
+ });
226
+
227
+ // src/auth/msal-cache-plugin.ts
228
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
229
+ import { dirname } from "path";
230
+ var MsalCachePlugin = class {
231
+ constructor(cacheFilePath) {
232
+ this.cacheFilePath = cacheFilePath;
233
+ }
234
+ async beforeCacheAccess(ctx) {
235
+ try {
236
+ const data = readFileSync(this.cacheFilePath, "utf-8");
237
+ ctx.tokenCache.deserialize(data);
238
+ } catch {
239
+ }
240
+ }
241
+ async afterCacheAccess(ctx) {
242
+ if (ctx.cacheHasChanged) {
243
+ mkdirSync(dirname(this.cacheFilePath), { recursive: true });
244
+ writeFileSync(this.cacheFilePath, ctx.tokenCache.serialize(), { encoding: "utf-8", mode: 384 });
245
+ }
246
+ }
247
+ };
248
+
249
+ // src/utils/browser.ts
250
+ import { execFile } from "child_process";
251
+ function openBrowser(url) {
252
+ const platform = process.platform;
253
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
254
+ execFile(cmd, [url], (err) => {
255
+ if (err) {
256
+ console.error(`Failed to open browser: ${err.message}`);
257
+ console.error(`Please open this URL manually: ${url}`);
258
+ }
259
+ });
260
+ }
261
+
262
+ // src/auth/auth-manager.ts
263
+ var CONFIG_DIR = ".dvx";
264
+ var CONFIG_FILE = "config.json";
265
+ var AuthManager = class {
266
+ configPath;
267
+ basePath;
268
+ config;
269
+ constructor(basePath) {
270
+ this.basePath = basePath ?? process.cwd();
271
+ this.configPath = path.join(this.basePath, CONFIG_DIR, CONFIG_FILE);
272
+ this.config = this.loadConfig();
273
+ }
274
+ loadConfig() {
275
+ try {
276
+ const raw = fs.readFileSync(this.configPath, "utf-8");
277
+ return AuthConfigSchema.parse(JSON.parse(raw));
278
+ } catch {
279
+ return { profiles: {} };
280
+ }
281
+ }
282
+ saveConfig() {
283
+ const dir = path.dirname(this.configPath);
284
+ fs.mkdirSync(dir, { recursive: true });
285
+ const safeConfig = {
286
+ activeProfile: this.config.activeProfile,
287
+ profiles: {}
288
+ };
289
+ for (const [name, profile] of Object.entries(this.config.profiles)) {
290
+ const { clientSecret: _secret, ...rest } = profile;
291
+ safeConfig.profiles[name] = rest;
292
+ }
293
+ fs.writeFileSync(this.configPath, JSON.stringify(safeConfig, null, 2));
294
+ }
295
+ createProfile(profile) {
296
+ if (this.config.profiles[profile.name]) {
297
+ throw new AuthProfileExistsError(profile.name);
298
+ }
299
+ this.config.profiles[profile.name] = profile;
300
+ if (!this.config.activeProfile) {
301
+ this.config.activeProfile = profile.name;
302
+ }
303
+ this.saveConfig();
304
+ }
305
+ getActiveProfile() {
306
+ const name = this.config.activeProfile;
307
+ if (!name || !this.config.profiles[name]) {
308
+ throw new AuthProfileNotFoundError(name ?? "default");
309
+ }
310
+ return this.config.profiles[name];
311
+ }
312
+ listProfiles() {
313
+ return Object.entries(this.config.profiles).map(([name, profile]) => ({
314
+ name,
315
+ active: name === this.config.activeProfile,
316
+ profile
317
+ }));
318
+ }
319
+ selectProfile(name) {
320
+ if (!this.config.profiles[name]) {
321
+ throw new AuthProfileNotFoundError(name);
322
+ }
323
+ this.config.activeProfile = name;
324
+ this.saveConfig();
325
+ }
326
+ deleteProfile(name) {
327
+ if (!this.config.profiles[name]) {
328
+ throw new AuthProfileNotFoundError(name);
329
+ }
330
+ delete this.config.profiles[name];
331
+ if (this.config.activeProfile === name) {
332
+ const remaining = Object.keys(this.config.profiles);
333
+ this.config.activeProfile = remaining.length > 0 ? remaining[0] : void 0;
334
+ }
335
+ this.saveConfig();
336
+ }
337
+ deleteAllProfiles() {
338
+ this.config.profiles = {};
339
+ this.config.activeProfile = void 0;
340
+ this.saveConfig();
341
+ }
342
+ saveProfile(profile) {
343
+ this.config.profiles[profile.name] = profile;
344
+ this.saveConfig();
345
+ }
346
+ async getTokenDelegated(profile) {
347
+ const cacheFilePath = path.join(this.basePath, ".dvx", "msal-cache.json");
348
+ const pca = new PublicClientApplication({
349
+ auth: {
350
+ clientId: profile.clientId,
351
+ authority: `https://login.microsoftonline.com/${profile.tenantId}`
352
+ },
353
+ cache: {
354
+ cachePlugin: new MsalCachePlugin(cacheFilePath)
355
+ }
356
+ });
357
+ const scopes = [`${profile.environmentUrl}/user_impersonation`];
358
+ const accounts = await pca.getAllAccounts();
359
+ if (accounts.length > 0 && accounts[0]) {
360
+ try {
361
+ const result2 = await pca.acquireTokenSilent({
362
+ account: accounts[0],
363
+ scopes
364
+ });
365
+ if (result2?.accessToken) return result2.accessToken;
366
+ } catch {
367
+ }
368
+ }
369
+ const result = await pca.acquireTokenInteractive({
370
+ scopes,
371
+ openBrowser: async (url) => {
372
+ openBrowser(url);
373
+ },
374
+ successTemplate: "<h1>Authentication complete. You may close this tab.</h1>",
375
+ errorTemplate: "<h1>Authentication failed: {error}</h1>"
376
+ });
377
+ if (!result?.accessToken) throw new PkceFlowError("No access token returned from interactive login");
378
+ if (result.account?.homeAccountId) {
379
+ profile.homeAccountId = result.account.homeAccountId;
380
+ this.saveProfile(profile);
381
+ }
382
+ return result.accessToken;
383
+ }
384
+ async getToken(profileOrName) {
385
+ let p;
386
+ if (typeof profileOrName === "string") {
387
+ if (!this.config.profiles[profileOrName]) {
388
+ throw new AuthProfileNotFoundError(profileOrName);
389
+ }
390
+ p = this.config.profiles[profileOrName];
391
+ } else {
392
+ p = profileOrName ?? this.getActiveProfile();
393
+ }
394
+ if (p.type === "delegated") {
395
+ return this.getTokenDelegated(p);
396
+ }
397
+ const clientSecret = p.clientSecret ?? process.env["DATAVERSE_CLIENT_SECRET"];
398
+ if (!clientSecret) {
399
+ throw new TokenAcquisitionError(
400
+ "Client secret not found. Set DATAVERSE_CLIENT_SECRET environment variable."
401
+ );
402
+ }
403
+ const app = new ConfidentialClientApplication({
404
+ auth: {
405
+ clientId: p.clientId,
406
+ clientSecret,
407
+ authority: `https://login.microsoftonline.com/${p.tenantId}`
408
+ }
409
+ });
410
+ const result = await app.acquireTokenByClientCredential({
411
+ scopes: [`${p.environmentUrl}/.default`]
412
+ });
413
+ if (!result?.accessToken) {
414
+ throw new TokenAcquisitionError("No access token returned from Entra ID");
415
+ }
416
+ return result.accessToken;
417
+ }
418
+ };
419
+
420
+ // src/client/dataverse-client.ts
421
+ import { z as z2 } from "zod";
422
+
423
+ // src/schema/schema-cache.ts
424
+ var DEFAULT_TTL_MS = 3e5;
425
+ var SchemaCache = class {
426
+ cache = /* @__PURE__ */ new Map();
427
+ ttlMs;
428
+ constructor(ttlMs) {
429
+ this.ttlMs = ttlMs ?? (Number(process.env["DVX_SCHEMA_CACHE_TTL_MS"]) || DEFAULT_TTL_MS);
430
+ }
431
+ get(entityName) {
432
+ const entry = this.cache.get(entityName.toLowerCase());
433
+ if (!entry) return void 0;
434
+ const age = Date.now() - entry.cachedAt.getTime();
435
+ if (age > entry.ttlMs) {
436
+ this.cache.delete(entityName.toLowerCase());
437
+ return void 0;
438
+ }
439
+ return entry;
440
+ }
441
+ set(entry) {
442
+ this.cache.set(entry.logicalName.toLowerCase(), {
443
+ ...entry,
444
+ cachedAt: /* @__PURE__ */ new Date(),
445
+ ttlMs: this.ttlMs
446
+ });
447
+ }
448
+ invalidate(entityName) {
449
+ this.cache.delete(entityName.toLowerCase());
450
+ }
451
+ clear() {
452
+ this.cache.clear();
453
+ }
454
+ };
455
+
456
+ // src/utils/retry.ts
457
+ var DEFAULT_OPTIONS = {
458
+ maxRetries: 3,
459
+ baseDelayMs: 1e3,
460
+ maxDelayMs: 3e4
461
+ };
462
+ async function withRetry(fn, options = {}) {
463
+ const opts = { ...DEFAULT_OPTIONS, ...options };
464
+ let lastError;
465
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
466
+ try {
467
+ return await fn();
468
+ } catch (error) {
469
+ lastError = error;
470
+ if (attempt === opts.maxRetries) break;
471
+ if (error instanceof DataverseError) {
472
+ if (error.statusCode === 429 || error.statusCode >= 500) {
473
+ const delay = error.retryAfterSeconds ? Math.min(error.retryAfterSeconds * 1e3, opts.maxDelayMs) : Math.min(opts.baseDelayMs * Math.pow(2, attempt), opts.maxDelayMs);
474
+ opts.onRetry?.(attempt + 1, delay, error);
475
+ await sleep(delay);
476
+ continue;
477
+ }
478
+ }
479
+ throw error;
480
+ }
481
+ }
482
+ throw lastError;
483
+ }
484
+ function sleep(ms) {
485
+ return new Promise((resolve) => setTimeout(resolve, ms));
486
+ }
487
+
488
+ // src/utils/validation.ts
489
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
490
+ var ENTITY_NAME_REGEX = /^[a-z][a-z0-9_]*$/i;
491
+ var DANGEROUS_CHARS = /[?#%]/;
492
+ function validateGuid(value, label = "ID") {
493
+ if (!UUID_REGEX.test(value)) {
494
+ throw new ValidationError(`Invalid GUID for ${label}: '${value}'`);
495
+ }
496
+ return value.toLowerCase();
497
+ }
498
+ function validateEntityName(name) {
499
+ if (DANGEROUS_CHARS.test(name)) {
500
+ throw new ValidationError(`Entity name contains invalid characters: '${name}'`);
501
+ }
502
+ if (!ENTITY_NAME_REGEX.test(name)) {
503
+ throw new ValidationError(`Invalid entity logical name: '${name}'`);
504
+ }
505
+ return name.toLowerCase();
506
+ }
507
+ function validateActionName(name) {
508
+ if (!/^[A-Z][A-Za-z0-9_]*$/.test(name)) {
509
+ throw new ValidationError(
510
+ `Invalid action name "${name}": must start with uppercase letter and contain only letters, digits, or underscores`
511
+ );
512
+ }
513
+ return name;
514
+ }
515
+ function validateUrl(url) {
516
+ try {
517
+ const parsed = new URL(url);
518
+ if (parsed.protocol !== "https:") {
519
+ throw new ValidationError(`Environment URL must use HTTPS: '${url}'`);
520
+ }
521
+ return parsed.origin;
522
+ } catch (e) {
523
+ if (e instanceof ValidationError) throw e;
524
+ throw new ValidationError(`Invalid URL: '${url}'`);
525
+ }
526
+ }
527
+
528
+ // src/utils/fetchxml.ts
529
+ import { XMLParser } from "fast-xml-parser";
530
+ var parser = new XMLParser({ ignoreAttributes: false });
531
+ function validateFetchXml(xml) {
532
+ let parsed;
533
+ try {
534
+ parsed = parser.parse(xml);
535
+ } catch (e) {
536
+ const message = e instanceof Error ? e.message : String(e);
537
+ throw new FetchXmlValidationError(`Invalid FetchXML: ${message}`);
538
+ }
539
+ if (!parsed["fetch"]) {
540
+ throw new FetchXmlValidationError("Invalid FetchXML: root element must be <fetch>");
541
+ }
542
+ }
543
+ function injectPagingCookie(xml, cookie, page) {
544
+ const decoded = decodeURIComponent(cookie);
545
+ const cleaned = xml.replace(/<fetch([^>]*)>/, (_match, attrs) => {
546
+ const stripped = attrs.replace(/\s+paging-cookie="[^"]*"/g, "").replace(/\s+page="[^"]*"/g, "");
547
+ return `<fetch${stripped}>`;
548
+ });
549
+ return cleaned.replace(
550
+ /<fetch/,
551
+ `<fetch paging-cookie="${decoded.replace(/"/g, "&quot;")}" page="${page}"`
552
+ );
553
+ }
554
+
555
+ // src/client/dataverse-client.ts
556
+ var ODataResponseSchema = z2.object({
557
+ value: z2.array(z2.record(z2.string(), z2.unknown())),
558
+ "@odata.nextLink": z2.string().optional(),
559
+ "@odata.count": z2.number().optional()
560
+ });
561
+ var EntityDefinitionSchema = z2.object({
562
+ LogicalName: z2.string(),
563
+ DisplayName: z2.object({
564
+ UserLocalizedLabel: z2.object({ Label: z2.string() }).nullable().optional()
565
+ }),
566
+ EntitySetName: z2.string(),
567
+ PrimaryIdAttribute: z2.string(),
568
+ PrimaryNameAttribute: z2.string().nullable()
569
+ });
570
+ var EntityListResponseSchema = z2.object({
571
+ value: z2.array(EntityDefinitionSchema)
572
+ });
573
+ var SingleEntityDefinitionSchema = EntityDefinitionSchema.extend({
574
+ Attributes: z2.array(z2.object({
575
+ LogicalName: z2.string(),
576
+ DisplayName: z2.object({
577
+ UserLocalizedLabel: z2.object({ Label: z2.string() }).nullable().optional()
578
+ }),
579
+ AttributeType: z2.string(),
580
+ RequiredLevel: z2.object({ Value: z2.string() }),
581
+ IsCustomAttribute: z2.boolean(),
582
+ MaxLength: z2.number().optional(),
583
+ Targets: z2.array(z2.string()).optional()
584
+ }))
585
+ });
586
+ var DataverseClient = class {
587
+ authManager;
588
+ schemaCache;
589
+ baseUrl;
590
+ debug;
591
+ dryRun;
592
+ callerObjectId;
593
+ constructor(authManager, schemaCache, opts) {
594
+ this.authManager = authManager;
595
+ this.schemaCache = schemaCache ?? new SchemaCache();
596
+ this.debug = process.env["DVX_DEBUG"] === "true";
597
+ this.dryRun = opts?.dryRun ?? false;
598
+ this.callerObjectId = opts?.callerObjectId;
599
+ }
600
+ async getBaseUrl() {
601
+ if (this.baseUrl) return this.baseUrl;
602
+ const profile = this.authManager.getActiveProfile();
603
+ this.baseUrl = `${profile.environmentUrl}/api/data/v9.2`;
604
+ return this.baseUrl;
605
+ }
606
+ async request(url, options = {}) {
607
+ const token = await this.authManager.getToken();
608
+ const method = options.method ?? "GET";
609
+ const headers = {
610
+ "Authorization": `Bearer ${token}`,
611
+ "Accept": "application/json",
612
+ "OData-MaxVersion": "4.0",
613
+ "OData-Version": "4.0",
614
+ ...options.headers ?? {}
615
+ };
616
+ if (this.callerObjectId) {
617
+ headers["CallerObjectId"] = this.callerObjectId;
618
+ }
619
+ if (this.debug) {
620
+ console.error(`[DVX] ${method} ${url}`);
621
+ }
622
+ const response = await fetch(url, { ...options, headers });
623
+ if (!response.ok) {
624
+ let errorMessage = response.statusText;
625
+ let errorCode;
626
+ try {
627
+ const body = await response.json();
628
+ if (body.error) {
629
+ errorMessage = body.error.message ?? errorMessage;
630
+ errorCode = body.error.code;
631
+ }
632
+ } catch {
633
+ }
634
+ let retryAfterSeconds;
635
+ const retryAfterHeader = response.headers.get("Retry-After");
636
+ if (retryAfterHeader) {
637
+ const parsed = Number(retryAfterHeader);
638
+ if (!Number.isNaN(parsed) && parsed > 0) {
639
+ retryAfterSeconds = parsed;
640
+ }
641
+ }
642
+ if (response.status === 403 && this.callerObjectId && errorMessage.includes("prvActOnBehalfOfAnotherUser")) {
643
+ throw new ImpersonationPrivilegeError(errorMessage);
644
+ }
645
+ throw new DataverseError(errorMessage, response.status, errorCode, retryAfterSeconds);
646
+ }
647
+ return response;
648
+ }
649
+ async listEntities() {
650
+ const baseUrl = await this.getBaseUrl();
651
+ const url = `${baseUrl}/EntityDefinitions?$select=LogicalName,DisplayName,EntitySetName,PrimaryIdAttribute,PrimaryNameAttribute`;
652
+ const response = await withRetry(() => this.request(url));
653
+ const json = await response.json();
654
+ const parsed = EntityListResponseSchema.parse(json);
655
+ return parsed.value.map((e) => ({
656
+ logicalName: e.LogicalName,
657
+ displayName: e.DisplayName.UserLocalizedLabel?.Label ?? e.LogicalName,
658
+ entitySetName: e.EntitySetName
659
+ }));
660
+ }
661
+ async getEntitySchema(entityName, noCache = false) {
662
+ const name = validateEntityName(entityName);
663
+ if (!noCache) {
664
+ const cached = this.schemaCache.get(name);
665
+ if (cached) return cached;
666
+ }
667
+ const baseUrl = await this.getBaseUrl();
668
+ const url = `${baseUrl}/EntityDefinitions(LogicalName='${name}')?$expand=Attributes`;
669
+ const response = await withRetry(() => this.request(url));
670
+ const json = await response.json();
671
+ let parsed;
672
+ try {
673
+ parsed = SingleEntityDefinitionSchema.parse(json);
674
+ } catch {
675
+ throw new EntityNotFoundError(name);
676
+ }
677
+ const entry = {
678
+ logicalName: parsed.LogicalName,
679
+ displayName: parsed.DisplayName.UserLocalizedLabel?.Label ?? parsed.LogicalName,
680
+ entitySetName: parsed.EntitySetName,
681
+ primaryIdAttribute: parsed.PrimaryIdAttribute,
682
+ primaryNameAttribute: parsed.PrimaryNameAttribute ?? "",
683
+ attributes: parsed.Attributes.map((a) => ({
684
+ logicalName: a.LogicalName,
685
+ displayName: a.DisplayName.UserLocalizedLabel?.Label ?? a.LogicalName,
686
+ attributeType: a.AttributeType,
687
+ requiredLevel: a.RequiredLevel.Value,
688
+ isCustomAttribute: a.IsCustomAttribute,
689
+ maxLength: a.MaxLength,
690
+ targets: a.Targets
691
+ })),
692
+ cachedAt: /* @__PURE__ */ new Date(),
693
+ ttlMs: 0
694
+ // will be set by cache
695
+ };
696
+ this.schemaCache.set(entry);
697
+ return entry;
698
+ }
699
+ async query(entitySetName, odata, options = {}) {
700
+ const baseUrl = await this.getBaseUrl();
701
+ const maxRows = options.maxRows ?? (Number(process.env["DVX_MAX_ROWS"]) || 5e3);
702
+ let totalRecords = 0;
703
+ const records = [];
704
+ let url = `${baseUrl}/${entitySetName}?${odata}`;
705
+ if (options.fields?.length) {
706
+ const separator = odata.includes("$select") ? "" : `&$select=${options.fields.join(",")}`;
707
+ if (separator) url += separator;
708
+ }
709
+ let pageNumber = 0;
710
+ do {
711
+ const retryOpts = options.onRetry ? { onRetry: options.onRetry } : {};
712
+ const response = await withRetry(() => this.request(url), retryOpts);
713
+ const json = await response.json();
714
+ const parsed = ODataResponseSchema.parse(json);
715
+ for (const record of parsed.value) {
716
+ if (totalRecords >= maxRows) break;
717
+ totalRecords++;
718
+ if (options.onRecord) {
719
+ options.onRecord(record);
720
+ } else {
721
+ records.push(record);
722
+ }
723
+ }
724
+ pageNumber++;
725
+ options.onProgress?.({ recordCount: totalRecords, pageNumber });
726
+ if (totalRecords >= maxRows) break;
727
+ url = parsed["@odata.nextLink"] ?? "";
728
+ } while (url && options.pageAll);
729
+ return records;
730
+ }
731
+ async getRecord(entityName, id, fields) {
732
+ const name = validateEntityName(entityName);
733
+ const guid = validateGuid(id);
734
+ const schema = await this.getEntitySchema(name);
735
+ const baseUrl = await this.getBaseUrl();
736
+ let url = `${baseUrl}/${schema.entitySetName}(${guid})`;
737
+ if (fields?.length) {
738
+ url += `?$select=${fields.join(",")}`;
739
+ }
740
+ let response;
741
+ try {
742
+ response = await withRetry(() => this.request(url));
743
+ } catch (e) {
744
+ if (e instanceof DataverseError && e.statusCode === 404) {
745
+ throw new RecordNotFoundError(name, guid);
746
+ }
747
+ throw e;
748
+ }
749
+ const json = await response.json();
750
+ return json;
751
+ }
752
+ async createRecord(entityName, data) {
753
+ const name = validateEntityName(entityName);
754
+ const schema = await this.getEntitySchema(name);
755
+ const baseUrl = await this.getBaseUrl();
756
+ const url = `${baseUrl}/${schema.entitySetName}`;
757
+ if (this.dryRun) {
758
+ logDryRun("POST", url, data);
759
+ return "dry-run";
760
+ }
761
+ const response = await withRetry(() => this.request(url, {
762
+ method: "POST",
763
+ headers: { "Content-Type": "application/json" },
764
+ body: JSON.stringify(data)
765
+ }));
766
+ const entityIdHeader = response.headers.get("OData-EntityId") ?? "";
767
+ const match = /\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)/i.exec(entityIdHeader);
768
+ if (!match?.[1]) {
769
+ throw new DataverseError("Create succeeded but OData-EntityId header missing", 201);
770
+ }
771
+ return match[1];
772
+ }
773
+ async updateRecord(entityName, id, data) {
774
+ const name = validateEntityName(entityName);
775
+ const guid = validateGuid(id);
776
+ const schema = await this.getEntitySchema(name);
777
+ const baseUrl = await this.getBaseUrl();
778
+ const url = `${baseUrl}/${schema.entitySetName}(${guid})`;
779
+ if (this.dryRun) {
780
+ logDryRun("PATCH", url, data);
781
+ return;
782
+ }
783
+ await withRetry(() => this.request(url, {
784
+ method: "PATCH",
785
+ headers: { "Content-Type": "application/json" },
786
+ body: JSON.stringify(data)
787
+ }));
788
+ }
789
+ async deleteRecord(entityName, id) {
790
+ const name = validateEntityName(entityName);
791
+ const guid = validateGuid(id);
792
+ const schema = await this.getEntitySchema(name);
793
+ const baseUrl = await this.getBaseUrl();
794
+ const url = `${baseUrl}/${schema.entitySetName}(${guid})`;
795
+ if (this.dryRun) {
796
+ logDryRun("DELETE", url);
797
+ return;
798
+ }
799
+ await withRetry(() => this.request(url, { method: "DELETE" }));
800
+ }
801
+ async queryFetchXml(entityName, fetchXml, onRecord, options) {
802
+ const name = validateEntityName(entityName);
803
+ const schema = await this.getEntitySchema(name);
804
+ const baseUrl = await this.getBaseUrl();
805
+ const maxRows = Number(process.env["DVX_MAX_ROWS"]) || 5e3;
806
+ let totalRecords = 0;
807
+ const records = [];
808
+ let page = 1;
809
+ let currentXml = fetchXml;
810
+ do {
811
+ const encoded = encodeURIComponent(currentXml);
812
+ const url = `${baseUrl}/${schema.entitySetName}?fetchXml=${encoded}`;
813
+ const fetchRetryOpts = options?.onRetry ? { onRetry: options.onRetry } : {};
814
+ const response = await withRetry(() => this.request(url), fetchRetryOpts);
815
+ const json = await response.json();
816
+ const parsed = ODataResponseSchema.parse(json);
817
+ for (const record of parsed.value) {
818
+ if (totalRecords >= maxRows) break;
819
+ totalRecords++;
820
+ if (onRecord) {
821
+ onRecord(record);
822
+ } else {
823
+ records.push(record);
824
+ }
825
+ }
826
+ options?.onProgress?.({ recordCount: totalRecords, pageNumber: page });
827
+ if (totalRecords >= maxRows) break;
828
+ const cookie = json["@Microsoft.Dynamics.CRM.fetchxmlpagingcookie"];
829
+ if (!cookie) break;
830
+ page++;
831
+ currentXml = injectPagingCookie(currentXml, cookie, page);
832
+ } while (true);
833
+ return records;
834
+ }
835
+ async executeAction(actionName, payload, opts) {
836
+ validateActionName(actionName);
837
+ const baseUrl = await this.getBaseUrl();
838
+ let url;
839
+ if (opts?.entityName && opts?.id) {
840
+ const guid = validateGuid(opts.id);
841
+ const schema = await this.getEntitySchema(opts.entityName);
842
+ url = `${baseUrl}/${schema.entitySetName}(${guid})/Microsoft.Dynamics.CRM.${actionName}`;
843
+ } else {
844
+ url = `${baseUrl}/${actionName}`;
845
+ }
846
+ if (this.dryRun) {
847
+ logDryRun("POST", url, payload);
848
+ return null;
849
+ }
850
+ try {
851
+ return await withRetry(async () => {
852
+ const response = await this.request(url, {
853
+ method: "POST",
854
+ headers: { "Content-Type": "application/json" },
855
+ body: JSON.stringify(payload)
856
+ });
857
+ if (response.status === 204) return null;
858
+ const data = await response.json();
859
+ return z2.record(z2.string(), z2.unknown()).parse(data);
860
+ });
861
+ } catch (err) {
862
+ if (err instanceof DataverseError) {
863
+ throw new ActionError(err.message, err.statusCode);
864
+ }
865
+ throw err;
866
+ }
867
+ }
868
+ invalidateSchema(entityName) {
869
+ this.schemaCache.invalidate(entityName);
870
+ }
871
+ clearSchemaCache() {
872
+ this.schemaCache.clear();
873
+ }
874
+ async executeBatch(body, boundary) {
875
+ const baseUrl = await this.getBaseUrl();
876
+ const url = `${baseUrl}/$batch`;
877
+ if (this.dryRun) {
878
+ logDryRun("POST", url, { batchBodyLength: body.length });
879
+ return "";
880
+ }
881
+ const response = await withRetry(() => this.request(url, {
882
+ method: "POST",
883
+ headers: { "Content-Type": `multipart/mixed;boundary=${boundary}` },
884
+ body
885
+ }));
886
+ return await response.text();
887
+ }
888
+ };
889
+
890
+ // src/schema/sqlite-schema-cache.ts
891
+ import Database from "better-sqlite3";
892
+ import { mkdirSync as mkdirSync3 } from "fs";
893
+ import { dirname as dirname3 } from "path";
894
+ var SqliteSchemaCache = class {
895
+ db;
896
+ ttlMs;
897
+ constructor(dbPath, ttlMs = 3e5) {
898
+ mkdirSync3(dirname3(dbPath), { recursive: true });
899
+ this.db = new Database(dbPath);
900
+ this.ttlMs = ttlMs;
901
+ this.db.exec(`
902
+ CREATE TABLE IF NOT EXISTS schema_cache (
903
+ logical_name TEXT PRIMARY KEY,
904
+ data TEXT NOT NULL,
905
+ cached_at INTEGER NOT NULL,
906
+ ttl_ms INTEGER NOT NULL
907
+ )
908
+ `);
909
+ }
910
+ get(entityName) {
911
+ const row = this.db.prepare(
912
+ "SELECT data, cached_at, ttl_ms FROM schema_cache WHERE logical_name = ?"
913
+ ).get(entityName.toLowerCase());
914
+ if (!row) return void 0;
915
+ if (Date.now() - row.cached_at >= row.ttl_ms) {
916
+ this.db.prepare("DELETE FROM schema_cache WHERE logical_name = ?").run(entityName.toLowerCase());
917
+ return void 0;
918
+ }
919
+ const entry = JSON.parse(row.data);
920
+ entry.cachedAt = new Date(entry.cachedAt);
921
+ return entry;
922
+ }
923
+ set(entry) {
924
+ this.db.prepare(
925
+ "INSERT OR REPLACE INTO schema_cache (logical_name, data, cached_at, ttl_ms) VALUES (?, ?, ?, ?)"
926
+ ).run(entry.logicalName.toLowerCase(), JSON.stringify(entry), Date.now(), this.ttlMs);
927
+ }
928
+ invalidate(entityName) {
929
+ this.db.prepare("DELETE FROM schema_cache WHERE logical_name = ?").run(entityName.toLowerCase());
930
+ }
931
+ clear() {
932
+ this.db.prepare("DELETE FROM schema_cache").run();
933
+ }
934
+ close() {
935
+ this.db.close();
936
+ }
937
+ };
938
+
939
+ // src/client/create-client.ts
940
+ async function createClient(opts) {
941
+ const authManager = new AuthManager();
942
+ const dbPath = process.env["DVX_SCHEMA_CACHE_PATH"] ?? path2.join(process.cwd(), ".dvx", "cache.db");
943
+ const ttlMs = Number(process.env["DVX_SCHEMA_CACHE_TTL_MS"]) || 3e5;
944
+ const schemaCache = new SqliteSchemaCache(dbPath, ttlMs);
945
+ const client = new DataverseClient(authManager, schemaCache, opts);
946
+ return { authManager, client };
947
+ }
948
+
949
+ // src/utils/batch-builder.ts
950
+ function chunkArray(arr, size) {
951
+ const chunks = [];
952
+ for (let i = 0; i < arr.length; i += size) {
953
+ chunks.push(arr.slice(i, i + size));
954
+ }
955
+ return chunks;
956
+ }
957
+ function serializeOperation(op, boundary, autoContentId) {
958
+ const parts = [];
959
+ parts.push(`--${boundary}`);
960
+ parts.push("Content-Type: application/http");
961
+ parts.push("Content-Transfer-Encoding: binary");
962
+ const contentId = op.contentId ?? autoContentId;
963
+ if (contentId) {
964
+ parts.push(`Content-ID: ${contentId}`);
965
+ }
966
+ parts.push("");
967
+ parts.push(`${op.method} ${op.path} HTTP/1.1`);
968
+ const headers = {
969
+ "Content-Type": "application/json",
970
+ "Accept": "application/json",
971
+ ...op.headers ?? {}
972
+ };
973
+ const serializedBody = op.body !== void 0 ? JSON.stringify(op.body) : void 0;
974
+ if (serializedBody !== void 0) {
975
+ headers["Content-Length"] = String(Buffer.byteLength(serializedBody, "utf-8"));
976
+ }
977
+ for (const [key, value] of Object.entries(headers)) {
978
+ parts.push(`${key}: ${value}`);
979
+ }
980
+ parts.push("");
981
+ if (serializedBody !== void 0) {
982
+ parts.push(serializedBody);
983
+ }
984
+ parts.push("");
985
+ return parts;
986
+ }
987
+ function buildBatchBody(operations, batchBoundary, options) {
988
+ const parts = [];
989
+ const changesetSize = options?.changesetSize ?? 100;
990
+ if (options?.atomic) {
991
+ const chunks = chunkArray(operations, changesetSize);
992
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
993
+ const chunk = chunks[chunkIndex];
994
+ const changesetBoundary = `changeset_${batchBoundary}_${chunkIndex}`;
995
+ parts.push(`--${batchBoundary}`);
996
+ parts.push(`Content-Type: multipart/mixed;boundary=${changesetBoundary}`);
997
+ parts.push("");
998
+ for (let opIndex = 0; opIndex < chunk.length; opIndex++) {
999
+ const op = chunk[opIndex];
1000
+ parts.push(...serializeOperation(op, changesetBoundary, String(chunkIndex * changesetSize + opIndex + 1)));
1001
+ }
1002
+ parts.push(`--${changesetBoundary}--`);
1003
+ }
1004
+ } else {
1005
+ for (const op of operations) {
1006
+ parts.push(...serializeOperation(op, batchBoundary));
1007
+ }
1008
+ }
1009
+ parts.push(`--${batchBoundary}--`);
1010
+ return parts.join("\r\n");
1011
+ }
1012
+
1013
+ export {
1014
+ DataverseError,
1015
+ EntityNotFoundError,
1016
+ ValidationError,
1017
+ ImpersonationPrivilegeError,
1018
+ MsalCachePlugin,
1019
+ openBrowser,
1020
+ AuthManager,
1021
+ validateGuid,
1022
+ validateEntityName,
1023
+ validateUrl,
1024
+ validateFetchXml,
1025
+ setUxOptions,
1026
+ isInteractive,
1027
+ createSpinner,
1028
+ logSuccess,
1029
+ logError,
1030
+ logWarn,
1031
+ logInfo,
1032
+ logStep,
1033
+ logMutationSuccess,
1034
+ promptConfirmClack,
1035
+ promptUrl,
1036
+ stripAnsi,
1037
+ createClient,
1038
+ chunkArray,
1039
+ buildBatchBody
1040
+ };
1041
+ //# sourceMappingURL=chunk-QFYVVOAX.js.map