@xrmforge/typegen 0.3.0 → 0.5.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/dist/index.d.ts +99 -10
- package/dist/index.js +444 -108
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
13
13
|
ErrorCode2["META_SOLUTION_NOT_FOUND"] = "META_3002";
|
|
14
14
|
ErrorCode2["META_FORM_PARSE_FAILED"] = "META_3003";
|
|
15
15
|
ErrorCode2["META_ATTRIBUTE_UNKNOWN_TYPE"] = "META_3004";
|
|
16
|
+
ErrorCode2["META_VERSION_STAMP_EXPIRED"] = "META_3005";
|
|
16
17
|
ErrorCode2["GEN_OUTPUT_WRITE_FAILED"] = "GEN_4001";
|
|
17
18
|
ErrorCode2["GEN_TEMPLATE_FAILED"] = "GEN_4002";
|
|
18
19
|
ErrorCode2["GEN_INVALID_IDENTIFIER"] = "GEN_4003";
|
|
@@ -331,6 +332,8 @@ var DataverseHttpClient = class {
|
|
|
331
332
|
maxRateLimitRetries;
|
|
332
333
|
readOnly;
|
|
333
334
|
cachedToken = null;
|
|
335
|
+
/** Pending token refresh promise (prevents concurrent token requests) */
|
|
336
|
+
pendingTokenRefresh = null;
|
|
334
337
|
// Semaphore for concurrency control (non-recursive)
|
|
335
338
|
activeConcurrentRequests = 0;
|
|
336
339
|
waitQueue = [];
|
|
@@ -471,18 +474,32 @@ var DataverseHttpClient = class {
|
|
|
471
474
|
if (this.cachedToken && this.cachedToken.expiresAt - Date.now() > TOKEN_BUFFER_MS) {
|
|
472
475
|
return this.cachedToken.token;
|
|
473
476
|
}
|
|
477
|
+
if (this.pendingTokenRefresh) {
|
|
478
|
+
return this.pendingTokenRefresh;
|
|
479
|
+
}
|
|
480
|
+
this.pendingTokenRefresh = this.refreshToken();
|
|
481
|
+
try {
|
|
482
|
+
return await this.pendingTokenRefresh;
|
|
483
|
+
} finally {
|
|
484
|
+
this.pendingTokenRefresh = null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/** Internal: actually acquire a new token from the credential provider. */
|
|
488
|
+
async refreshToken() {
|
|
474
489
|
log2.debug("Requesting new access token");
|
|
475
490
|
const scope = `${this.baseUrl}/.default`;
|
|
476
491
|
let tokenResponse;
|
|
477
492
|
try {
|
|
478
493
|
tokenResponse = await this.credential.getToken(scope);
|
|
479
494
|
} catch (error) {
|
|
495
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
480
496
|
throw new AuthenticationError(
|
|
481
497
|
"AUTH_1003" /* AUTH_TOKEN_FAILED */,
|
|
482
|
-
`Failed to acquire access token for ${this.baseUrl}. Verify your authentication configuration
|
|
498
|
+
`Failed to acquire access token for ${this.baseUrl}. Verify your authentication configuration.
|
|
499
|
+
Cause: ${cause}`,
|
|
483
500
|
{
|
|
484
501
|
environmentUrl: this.baseUrl,
|
|
485
|
-
originalError:
|
|
502
|
+
originalError: cause
|
|
486
503
|
}
|
|
487
504
|
);
|
|
488
505
|
}
|
|
@@ -508,10 +525,10 @@ var DataverseHttpClient = class {
|
|
|
508
525
|
* The semaphore is acquired ONCE per logical request. Retries happen
|
|
509
526
|
* INSIDE the semaphore to avoid the recursive slot exhaustion bug.
|
|
510
527
|
*/
|
|
511
|
-
async executeWithConcurrency(url, signal) {
|
|
528
|
+
async executeWithConcurrency(url, signal, method = "GET", requestBody) {
|
|
512
529
|
await this.acquireSlot();
|
|
513
530
|
try {
|
|
514
|
-
return await this.executeWithRetry(url, 1, 0, signal);
|
|
531
|
+
return await this.executeWithRetry(url, 1, 0, signal, method, requestBody);
|
|
515
532
|
} finally {
|
|
516
533
|
this.releaseSlot();
|
|
517
534
|
}
|
|
@@ -521,10 +538,10 @@ var DataverseHttpClient = class {
|
|
|
521
538
|
this.activeConcurrentRequests++;
|
|
522
539
|
return Promise.resolve();
|
|
523
540
|
}
|
|
524
|
-
return new Promise((
|
|
541
|
+
return new Promise((resolve2) => {
|
|
525
542
|
this.waitQueue.push(() => {
|
|
526
543
|
this.activeConcurrentRequests++;
|
|
527
|
-
|
|
544
|
+
resolve2();
|
|
528
545
|
});
|
|
529
546
|
});
|
|
530
547
|
}
|
|
@@ -534,7 +551,7 @@ var DataverseHttpClient = class {
|
|
|
534
551
|
if (next) next();
|
|
535
552
|
}
|
|
536
553
|
// ─── Retry Logic (runs INSIDE a single concurrency slot) ─────────────────
|
|
537
|
-
async executeWithRetry(url, attempt, rateLimitRetries = 0, signal) {
|
|
554
|
+
async executeWithRetry(url, attempt, rateLimitRetries = 0, signal, method = "GET", requestBody) {
|
|
538
555
|
if (signal?.aborted) {
|
|
539
556
|
throw new ApiRequestError(
|
|
540
557
|
"API_2001" /* API_REQUEST_FAILED */,
|
|
@@ -549,17 +566,21 @@ var DataverseHttpClient = class {
|
|
|
549
566
|
signal?.addEventListener("abort", onUserAbort, { once: true });
|
|
550
567
|
let response;
|
|
551
568
|
try {
|
|
552
|
-
log2.debug(
|
|
569
|
+
log2.debug(`${method} ${url}`, { attempt });
|
|
570
|
+
const headers = {
|
|
571
|
+
Authorization: `Bearer ${token}`,
|
|
572
|
+
"OData-MaxVersion": "4.0",
|
|
573
|
+
"OData-Version": "4.0",
|
|
574
|
+
Accept: "application/json",
|
|
575
|
+
Prefer: 'odata.include-annotations="*"'
|
|
576
|
+
};
|
|
577
|
+
if (method === "POST") {
|
|
578
|
+
headers["Content-Type"] = "application/json";
|
|
579
|
+
}
|
|
553
580
|
response = await fetch(url, {
|
|
554
|
-
method
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
Authorization: `Bearer ${token}`,
|
|
558
|
-
"OData-MaxVersion": "4.0",
|
|
559
|
-
"OData-Version": "4.0",
|
|
560
|
-
Accept: "application/json",
|
|
561
|
-
Prefer: 'odata.include-annotations="*"'
|
|
562
|
-
},
|
|
581
|
+
method,
|
|
582
|
+
headers,
|
|
583
|
+
body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0,
|
|
563
584
|
signal: controller.signal
|
|
564
585
|
});
|
|
565
586
|
} catch (fetchError) {
|
|
@@ -579,7 +600,7 @@ var DataverseHttpClient = class {
|
|
|
579
600
|
url
|
|
580
601
|
});
|
|
581
602
|
await this.sleep(delay);
|
|
582
|
-
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal);
|
|
603
|
+
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal, method, requestBody);
|
|
583
604
|
}
|
|
584
605
|
throw new ApiRequestError(
|
|
585
606
|
"API_2005" /* API_TIMEOUT */,
|
|
@@ -594,7 +615,7 @@ var DataverseHttpClient = class {
|
|
|
594
615
|
error: fetchError instanceof Error ? fetchError.message : String(fetchError)
|
|
595
616
|
});
|
|
596
617
|
await this.sleep(delay);
|
|
597
|
-
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal);
|
|
618
|
+
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal, method, requestBody);
|
|
598
619
|
}
|
|
599
620
|
throw new ApiRequestError(
|
|
600
621
|
"API_2001" /* API_REQUEST_FAILED */,
|
|
@@ -609,12 +630,12 @@ var DataverseHttpClient = class {
|
|
|
609
630
|
signal?.removeEventListener("abort", onUserAbort);
|
|
610
631
|
}
|
|
611
632
|
if (!response.ok) {
|
|
612
|
-
return this.handleHttpError(response, url, attempt, rateLimitRetries, signal);
|
|
633
|
+
return this.handleHttpError(response, url, attempt, rateLimitRetries, signal, method, requestBody);
|
|
613
634
|
}
|
|
614
|
-
log2.debug(
|
|
635
|
+
log2.debug(`${method} ${url} -> ${response.status}`, { attempt });
|
|
615
636
|
return response.json();
|
|
616
637
|
}
|
|
617
|
-
async handleHttpError(response, url, attempt, rateLimitRetries, signal) {
|
|
638
|
+
async handleHttpError(response, url, attempt, rateLimitRetries, signal, method = "GET", requestBody) {
|
|
618
639
|
const body = await response.text();
|
|
619
640
|
if (response.status === 429) {
|
|
620
641
|
if (rateLimitRetries >= this.maxRateLimitRetries) {
|
|
@@ -631,12 +652,12 @@ var DataverseHttpClient = class {
|
|
|
631
652
|
retryAfterHeader
|
|
632
653
|
});
|
|
633
654
|
await this.sleep(retryAfterMs);
|
|
634
|
-
return this.executeWithRetry(url, attempt, rateLimitRetries + 1, signal);
|
|
655
|
+
return this.executeWithRetry(url, attempt, rateLimitRetries + 1, signal, method, requestBody);
|
|
635
656
|
}
|
|
636
657
|
if (response.status === 401 && attempt === 1) {
|
|
637
658
|
log2.warn("HTTP 401 received, clearing token cache and retrying");
|
|
638
659
|
this.cachedToken = null;
|
|
639
|
-
return this.executeWithRetry(url, attempt + 1, 0, signal);
|
|
660
|
+
return this.executeWithRetry(url, attempt + 1, 0, signal, method, requestBody);
|
|
640
661
|
}
|
|
641
662
|
if (response.status >= 500 && attempt <= this.maxRetries) {
|
|
642
663
|
const delay = this.calculateBackoff(attempt);
|
|
@@ -644,7 +665,7 @@ var DataverseHttpClient = class {
|
|
|
644
665
|
`Server error ${response.status}, retrying in ${delay}ms (${attempt}/${this.maxRetries})`
|
|
645
666
|
);
|
|
646
667
|
await this.sleep(delay);
|
|
647
|
-
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal);
|
|
668
|
+
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal, method, requestBody);
|
|
648
669
|
}
|
|
649
670
|
const errorCode = response.status === 401 ? "API_2004" /* API_UNAUTHORIZED */ : response.status === 404 ? "API_2003" /* API_NOT_FOUND */ : "API_2001" /* API_REQUEST_FAILED */;
|
|
650
671
|
throw new ApiRequestError(
|
|
@@ -667,7 +688,7 @@ var DataverseHttpClient = class {
|
|
|
667
688
|
return Math.min(exponential + jitter, MAX_BACKOFF_MS);
|
|
668
689
|
}
|
|
669
690
|
sleep(ms) {
|
|
670
|
-
return new Promise((
|
|
691
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
671
692
|
}
|
|
672
693
|
};
|
|
673
694
|
|
|
@@ -1243,10 +1264,12 @@ var MetadataCache = class {
|
|
|
1243
1264
|
cacheDir;
|
|
1244
1265
|
cacheFilePath;
|
|
1245
1266
|
/**
|
|
1246
|
-
* @param
|
|
1267
|
+
* @param cacheDir - Directory where cache files are stored.
|
|
1268
|
+
* Can be an absolute path or relative to cwd.
|
|
1269
|
+
* Defaults to ".xrmforge/cache" when constructed without argument.
|
|
1247
1270
|
*/
|
|
1248
|
-
constructor(
|
|
1249
|
-
this.cacheDir = path.
|
|
1271
|
+
constructor(cacheDir = CACHE_DIR) {
|
|
1272
|
+
this.cacheDir = path.resolve(cacheDir);
|
|
1250
1273
|
this.cacheFilePath = path.join(this.cacheDir, CACHE_FILE);
|
|
1251
1274
|
}
|
|
1252
1275
|
/**
|
|
@@ -1362,6 +1385,98 @@ var MetadataCache = class {
|
|
|
1362
1385
|
}
|
|
1363
1386
|
};
|
|
1364
1387
|
|
|
1388
|
+
// src/metadata/change-detector.ts
|
|
1389
|
+
var log6 = createLogger("change-detector");
|
|
1390
|
+
var EXPIRED_VERSION_STAMP_ERROR = "0x80044352";
|
|
1391
|
+
var ChangeDetector = class {
|
|
1392
|
+
constructor(http) {
|
|
1393
|
+
this.http = http;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Detect which entities have changed since the given version stamp.
|
|
1397
|
+
*
|
|
1398
|
+
* @param clientVersionStamp - The ServerVersionStamp from the last run (from cache)
|
|
1399
|
+
* @returns Changed entity names, deleted entity names, and new version stamp
|
|
1400
|
+
* @throws {MetadataError} with META_VERSION_STAMP_EXPIRED if stamp is too old (>90 days)
|
|
1401
|
+
*/
|
|
1402
|
+
async detectChanges(clientVersionStamp) {
|
|
1403
|
+
log6.info("Detecting metadata changes since last run");
|
|
1404
|
+
const query = {
|
|
1405
|
+
Criteria: {
|
|
1406
|
+
FilterOperator: "And",
|
|
1407
|
+
Conditions: []
|
|
1408
|
+
},
|
|
1409
|
+
Properties: {
|
|
1410
|
+
AllProperties: false,
|
|
1411
|
+
PropertyNames: ["LogicalName"]
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
const queryJson = encodeURIComponent(JSON.stringify(query));
|
|
1415
|
+
const deletedFilter = `Microsoft.Dynamics.CRM.DeletedMetadataFilters'Default'`;
|
|
1416
|
+
const path2 = `/RetrieveMetadataChanges(Query=@q,ClientVersionStamp=@s,DeletedMetadataFilters=@d)?@q=${queryJson}&@s='${clientVersionStamp}'&@d=${deletedFilter}`;
|
|
1417
|
+
let response;
|
|
1418
|
+
try {
|
|
1419
|
+
response = await this.http.get(path2);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (this.isExpiredVersionStampError(error)) {
|
|
1422
|
+
throw new MetadataError(
|
|
1423
|
+
"META_3005" /* META_VERSION_STAMP_EXPIRED */,
|
|
1424
|
+
"Cached version stamp has expired (>90 days). A full metadata refresh is required.",
|
|
1425
|
+
{ clientVersionStamp }
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
const changedEntityNames = (response.EntityMetadata ?? []).filter((e) => e.HasChanged !== false).map((e) => e.LogicalName).filter(Boolean);
|
|
1431
|
+
const deletedEntityNames = [];
|
|
1432
|
+
if (response.DeletedMetadata?.Keys) {
|
|
1433
|
+
log6.info(`${response.DeletedMetadata.Keys.length} entity metadata IDs were deleted`);
|
|
1434
|
+
}
|
|
1435
|
+
const newVersionStamp = response.ServerVersionStamp;
|
|
1436
|
+
log6.info(`Change detection complete: ${changedEntityNames.length} changed, ${deletedEntityNames.length} deleted`, {
|
|
1437
|
+
changedEntityNames: changedEntityNames.length <= 10 ? changedEntityNames : `${changedEntityNames.length} entities`,
|
|
1438
|
+
newVersionStamp: newVersionStamp.substring(0, 20) + "..."
|
|
1439
|
+
});
|
|
1440
|
+
return {
|
|
1441
|
+
changedEntityNames,
|
|
1442
|
+
deletedEntityNames,
|
|
1443
|
+
newVersionStamp
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Perform an initial metadata query to get the first ServerVersionStamp.
|
|
1448
|
+
* This is used on the very first run (no cache exists).
|
|
1449
|
+
*
|
|
1450
|
+
* @returns The initial server version stamp
|
|
1451
|
+
*/
|
|
1452
|
+
async getInitialVersionStamp() {
|
|
1453
|
+
log6.info("Fetching initial server version stamp");
|
|
1454
|
+
const query = {
|
|
1455
|
+
Criteria: {
|
|
1456
|
+
FilterOperator: "And",
|
|
1457
|
+
Conditions: []
|
|
1458
|
+
},
|
|
1459
|
+
Properties: {
|
|
1460
|
+
AllProperties: false,
|
|
1461
|
+
PropertyNames: ["LogicalName"]
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
const queryParam = encodeURIComponent(JSON.stringify(query));
|
|
1465
|
+
const path2 = `/RetrieveMetadataChanges(Query=@q)?@q=${queryParam}`;
|
|
1466
|
+
const response = await this.http.get(path2);
|
|
1467
|
+
log6.info("Initial version stamp acquired");
|
|
1468
|
+
return response.ServerVersionStamp;
|
|
1469
|
+
}
|
|
1470
|
+
/** Check if an error is the expired version stamp error (0x80044352) */
|
|
1471
|
+
isExpiredVersionStampError(error) {
|
|
1472
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1473
|
+
const errorRecord = error;
|
|
1474
|
+
const contextBody = errorRecord?.context ? String(errorRecord.context["responseBody"] ?? "") : "";
|
|
1475
|
+
const combined = msg + contextBody;
|
|
1476
|
+
return combined.includes(EXPIRED_VERSION_STAMP_ERROR) || combined.includes("ExpiredVersionStamp");
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1365
1480
|
// src/metadata/labels.ts
|
|
1366
1481
|
var DEFAULT_LABEL_CONFIG = {
|
|
1367
1482
|
primaryLanguage: 1033
|
|
@@ -1455,10 +1570,12 @@ function getLabelLanguagesParam(config) {
|
|
|
1455
1570
|
}
|
|
1456
1571
|
|
|
1457
1572
|
// src/generators/type-mapping.ts
|
|
1573
|
+
var log7 = createLogger("type-mapping");
|
|
1458
1574
|
function getEntityPropertyType(attributeType, isLookup = false) {
|
|
1459
1575
|
if (isLookup) return "string";
|
|
1460
1576
|
const mapping = ENTITY_TYPE_MAP[attributeType];
|
|
1461
1577
|
if (mapping) return mapping;
|
|
1578
|
+
log7.warn(`Unmapped AttributeType "${attributeType}" falling back to "unknown"`);
|
|
1462
1579
|
return "unknown";
|
|
1463
1580
|
}
|
|
1464
1581
|
var ENTITY_TYPE_MAP = {
|
|
@@ -1497,6 +1614,7 @@ var ENTITY_TYPE_MAP = {
|
|
|
1497
1614
|
function getFormAttributeType(attributeType) {
|
|
1498
1615
|
const mapping = FORM_ATTRIBUTE_TYPE_MAP[attributeType];
|
|
1499
1616
|
if (mapping) return mapping;
|
|
1617
|
+
log7.warn(`Unmapped form AttributeType "${attributeType}" falling back to generic Attribute`);
|
|
1500
1618
|
return "Xrm.Attributes.Attribute";
|
|
1501
1619
|
}
|
|
1502
1620
|
var FORM_ATTRIBUTE_TYPE_MAP = {
|
|
@@ -1990,6 +2108,50 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
|
|
|
1990
2108
|
lines.push("");
|
|
1991
2109
|
}
|
|
1992
2110
|
}
|
|
2111
|
+
const specialControls = form.allSpecialControls || [];
|
|
2112
|
+
const subgrids = specialControls.filter((sc) => sc.controlType === "subgrid" || sc.controlType === "editablegrid");
|
|
2113
|
+
const quickViews = specialControls.filter((sc) => sc.controlType === "quickview");
|
|
2114
|
+
if (subgrids.length > 0) {
|
|
2115
|
+
const subgridsEnumName = `${baseName}FormSubgrids`;
|
|
2116
|
+
lines.push(` /** Subgrid constants for "${form.name}" (compile-time only, zero runtime) */`);
|
|
2117
|
+
lines.push(` const enum ${subgridsEnumName} {`);
|
|
2118
|
+
const usedMembers = /* @__PURE__ */ new Set();
|
|
2119
|
+
for (const sg of subgrids) {
|
|
2120
|
+
let member = toSafeFormName(sg.id) || toPascalCase(sg.id);
|
|
2121
|
+
const original = member;
|
|
2122
|
+
let counter = 2;
|
|
2123
|
+
while (usedMembers.has(member)) {
|
|
2124
|
+
member = `${original}${counter}`;
|
|
2125
|
+
counter++;
|
|
2126
|
+
}
|
|
2127
|
+
usedMembers.add(member);
|
|
2128
|
+
const label = sg.targetEntityType ? `Subgrid: ${sg.targetEntityType}` : `Subgrid`;
|
|
2129
|
+
lines.push(` /** ${label} */`);
|
|
2130
|
+
lines.push(` ${member} = '${sg.id}',`);
|
|
2131
|
+
}
|
|
2132
|
+
lines.push(" }");
|
|
2133
|
+
lines.push("");
|
|
2134
|
+
}
|
|
2135
|
+
if (quickViews.length > 0) {
|
|
2136
|
+
const qvEnumName = `${baseName}FormQuickViews`;
|
|
2137
|
+
lines.push(` /** Quick View constants for "${form.name}" (compile-time only, zero runtime) */`);
|
|
2138
|
+
lines.push(` const enum ${qvEnumName} {`);
|
|
2139
|
+
const usedMembers = /* @__PURE__ */ new Set();
|
|
2140
|
+
for (const qv of quickViews) {
|
|
2141
|
+
let member = toSafeFormName(qv.id) || toPascalCase(qv.id);
|
|
2142
|
+
const original = member;
|
|
2143
|
+
let counter = 2;
|
|
2144
|
+
while (usedMembers.has(member)) {
|
|
2145
|
+
member = `${original}${counter}`;
|
|
2146
|
+
counter++;
|
|
2147
|
+
}
|
|
2148
|
+
usedMembers.add(member);
|
|
2149
|
+
lines.push(` /** Quick View */`);
|
|
2150
|
+
lines.push(` ${member} = '${qv.id}',`);
|
|
2151
|
+
}
|
|
2152
|
+
lines.push(" }");
|
|
2153
|
+
lines.push("");
|
|
2154
|
+
}
|
|
1993
2155
|
lines.push(` /** ${form.name} */`);
|
|
1994
2156
|
lines.push(` interface ${interfaceName} extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {`);
|
|
1995
2157
|
lines.push(` /** Typisierter Feldzugriff: nur Felder die auf diesem Formular existieren */`);
|
|
@@ -1999,7 +2161,6 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
|
|
|
1999
2161
|
lines.push("");
|
|
2000
2162
|
lines.push(` /** Typisierter Control-Zugriff: nur Controls die auf diesem Formular existieren */`);
|
|
2001
2163
|
lines.push(` getControl<K extends ${fieldsTypeName}>(name: K): ${ctrlMapName}[K];`);
|
|
2002
|
-
const specialControls = form.allSpecialControls || [];
|
|
2003
2164
|
for (const sc of specialControls) {
|
|
2004
2165
|
const xrmType = specialControlToXrmType(sc.controlType);
|
|
2005
2166
|
if (xrmType) {
|
|
@@ -2380,6 +2541,7 @@ function createBoundAction(operationName, entityLogicalName, paramMeta) {
|
|
|
2380
2541
|
}
|
|
2381
2542
|
function createUnboundAction(operationName, paramMeta) {
|
|
2382
2543
|
return {
|
|
2544
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- return type varies (Response or parsed JSON)
|
|
2383
2545
|
async execute(params) {
|
|
2384
2546
|
const req = buildUnboundRequest(
|
|
2385
2547
|
operationName,
|
|
@@ -2664,7 +2826,7 @@ function groupCustomApis(apis) {
|
|
|
2664
2826
|
}
|
|
2665
2827
|
|
|
2666
2828
|
// src/orchestrator/file-writer.ts
|
|
2667
|
-
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
2829
|
+
import { mkdir, writeFile, readFile, unlink } from "fs/promises";
|
|
2668
2830
|
import { join as join2, dirname } from "path";
|
|
2669
2831
|
async function writeGeneratedFile(outputDir, file) {
|
|
2670
2832
|
const absolutePath = join2(outputDir, file.relativePath);
|
|
@@ -2696,6 +2858,24 @@ async function writeAllFiles(outputDir, files) {
|
|
|
2696
2858
|
}
|
|
2697
2859
|
return result;
|
|
2698
2860
|
}
|
|
2861
|
+
async function deleteOrphanedFiles(outputDir, deletedEntityNames) {
|
|
2862
|
+
let deleted = 0;
|
|
2863
|
+
const subdirs = ["entities", "optionsets", "forms"];
|
|
2864
|
+
for (const entityName of deletedEntityNames) {
|
|
2865
|
+
for (const subdir of subdirs) {
|
|
2866
|
+
const filePath = join2(outputDir, subdir, `${entityName}.d.ts`);
|
|
2867
|
+
try {
|
|
2868
|
+
await unlink(filePath);
|
|
2869
|
+
deleted++;
|
|
2870
|
+
} catch (error) {
|
|
2871
|
+
if (error.code !== "ENOENT") {
|
|
2872
|
+
throw error;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
return deleted;
|
|
2878
|
+
}
|
|
2699
2879
|
var GENERATED_HEADER = `// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2700
2880
|
// This file was generated by @xrmforge/typegen. Do not edit manually.
|
|
2701
2881
|
// Re-run 'xrmforge generate' to update.
|
|
@@ -2742,11 +2922,6 @@ var TypeGenerationOrchestrator = class {
|
|
|
2742
2922
|
constructor(credential, config, logger) {
|
|
2743
2923
|
this.credential = credential;
|
|
2744
2924
|
this.logger = logger ?? createLogger("orchestrator");
|
|
2745
|
-
if (config.useCache) {
|
|
2746
|
-
throw new Error(
|
|
2747
|
-
"Metadata caching is not yet implemented (planned for v0.2.0). Remove the useCache option or set it to false."
|
|
2748
|
-
);
|
|
2749
|
-
}
|
|
2750
2925
|
this.config = {
|
|
2751
2926
|
environmentUrl: config.environmentUrl,
|
|
2752
2927
|
entities: [...config.entities],
|
|
@@ -2781,7 +2956,8 @@ var TypeGenerationOrchestrator = class {
|
|
|
2781
2956
|
}
|
|
2782
2957
|
this.logger.info("Starting type generation", {
|
|
2783
2958
|
entities: this.config.entities,
|
|
2784
|
-
outputDir: this.config.outputDir
|
|
2959
|
+
outputDir: this.config.outputDir,
|
|
2960
|
+
useCache: this.config.useCache
|
|
2785
2961
|
});
|
|
2786
2962
|
const httpClient = new DataverseHttpClient({
|
|
2787
2963
|
environmentUrl: this.config.environmentUrl,
|
|
@@ -2804,80 +2980,71 @@ var TypeGenerationOrchestrator = class {
|
|
|
2804
2980
|
durationMs: Date.now() - startTime
|
|
2805
2981
|
};
|
|
2806
2982
|
}
|
|
2807
|
-
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2983
|
+
let cacheStats;
|
|
2984
|
+
const entitiesToFetch = new Set(this.config.entities);
|
|
2985
|
+
const cachedEntityInfos = {};
|
|
2986
|
+
let cache;
|
|
2987
|
+
let newVersionStamp = null;
|
|
2988
|
+
const deletedEntityNames = [];
|
|
2989
|
+
if (this.config.useCache) {
|
|
2990
|
+
const cacheResult = await this.resolveCache(httpClient, entitiesToFetch);
|
|
2991
|
+
cache = cacheResult.cache;
|
|
2992
|
+
newVersionStamp = cacheResult.newVersionStamp;
|
|
2993
|
+
cacheStats = cacheResult.stats;
|
|
2994
|
+
for (const [name, info] of Object.entries(cacheResult.cachedEntities)) {
|
|
2995
|
+
cachedEntityInfos[name] = info;
|
|
2996
|
+
entitiesToFetch.delete(name);
|
|
2997
|
+
}
|
|
2998
|
+
deletedEntityNames.push(...cacheResult.deletedEntityNames);
|
|
2999
|
+
}
|
|
3000
|
+
const fetchList = [...entitiesToFetch];
|
|
3001
|
+
const failedEntities = /* @__PURE__ */ new Map();
|
|
3002
|
+
if (fetchList.length > 0) {
|
|
3003
|
+
this.logger.info(`Fetching ${fetchList.length} entities from Dataverse`);
|
|
3004
|
+
const settled = await Promise.allSettled(
|
|
3005
|
+
fetchList.map((entityName) => {
|
|
3006
|
+
if (signal?.aborted) {
|
|
3007
|
+
return Promise.reject(new Error("Generation aborted"));
|
|
3008
|
+
}
|
|
3009
|
+
return metadataClient.getEntityTypeInfo(entityName).then((info) => {
|
|
3010
|
+
this.logger.info(`Fetched entity: ${entityName}`);
|
|
3011
|
+
return { entityName, info };
|
|
3012
|
+
});
|
|
3013
|
+
})
|
|
3014
|
+
);
|
|
3015
|
+
for (let i = 0; i < settled.length; i++) {
|
|
3016
|
+
const outcome = settled[i];
|
|
3017
|
+
const entityName = fetchList[i];
|
|
3018
|
+
if (outcome.status === "fulfilled") {
|
|
3019
|
+
cachedEntityInfos[outcome.value.entityName] = outcome.value.info;
|
|
3020
|
+
} else {
|
|
3021
|
+
const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
3022
|
+
this.logger.error(`Failed to fetch entity: ${entityName}`, { error: outcome.reason });
|
|
3023
|
+
failedEntities.set(entityName, errorMsg);
|
|
2812
3024
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
for (let i = 0; i < settled.length; i++) {
|
|
2820
|
-
const outcome = settled[i];
|
|
2821
|
-
const entityName = this.config.entities[i];
|
|
2822
|
-
if (outcome.status === "fulfilled") {
|
|
2823
|
-
entityResults.push(outcome.value);
|
|
2824
|
-
allFiles.push(...outcome.value.files);
|
|
2825
|
-
} else {
|
|
2826
|
-
const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
2827
|
-
this.logger.error(`Failed to process entity: ${entityName}`, { error: outcome.reason });
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
this.logger.info(`Generating types for ${this.config.entities.length - failedEntities.size} entities`);
|
|
3028
|
+
for (const entityName of this.config.entities) {
|
|
3029
|
+
if (signal?.aborted) break;
|
|
3030
|
+
if (failedEntities.has(entityName)) {
|
|
2828
3031
|
entityResults.push({
|
|
2829
3032
|
entityLogicalName: entityName,
|
|
2830
3033
|
files: [],
|
|
2831
|
-
warnings: [`Failed to process: ${
|
|
3034
|
+
warnings: [`Failed to process: ${failedEntities.get(entityName)}`]
|
|
2832
3035
|
});
|
|
3036
|
+
continue;
|
|
2833
3037
|
}
|
|
3038
|
+
const entityInfo = cachedEntityInfos[entityName];
|
|
3039
|
+
if (!entityInfo) continue;
|
|
3040
|
+
const result = this.generateEntityFiles(entityName, entityInfo);
|
|
3041
|
+
this.logger.info(`Generated entity: ${entityName} (${result.files.length} files)`);
|
|
3042
|
+
entityResults.push(result);
|
|
3043
|
+
allFiles.push(...result.files);
|
|
2834
3044
|
}
|
|
2835
3045
|
if (this.config.generateActions && !signal?.aborted) {
|
|
2836
|
-
this.
|
|
2837
|
-
|
|
2838
|
-
if (this.config.actionsFilter) {
|
|
2839
|
-
const prefix = this.config.actionsFilter.toLowerCase();
|
|
2840
|
-
const before = customApis.length;
|
|
2841
|
-
customApis = customApis.filter((api) => api.api.uniquename.toLowerCase().startsWith(prefix));
|
|
2842
|
-
this.logger.info(`Filtered Custom APIs by prefix "${this.config.actionsFilter}": ${before} -> ${customApis.length}`);
|
|
2843
|
-
}
|
|
2844
|
-
if (customApis.length > 0) {
|
|
2845
|
-
const importPath = "@xrmforge/typegen";
|
|
2846
|
-
const grouped = groupCustomApis(customApis);
|
|
2847
|
-
for (const [key, apis] of grouped.actions) {
|
|
2848
|
-
const entityName = key === "global" ? void 0 : key;
|
|
2849
|
-
const declarations = generateActionDeclarations(apis, false, entityName, { importPath });
|
|
2850
|
-
const module = generateActionModule(apis, false, { importPath });
|
|
2851
|
-
allFiles.push({
|
|
2852
|
-
relativePath: `actions/${key}.d.ts`,
|
|
2853
|
-
content: addGeneratedHeader(declarations),
|
|
2854
|
-
type: "action"
|
|
2855
|
-
});
|
|
2856
|
-
allFiles.push({
|
|
2857
|
-
relativePath: `actions/${key}.ts`,
|
|
2858
|
-
content: addGeneratedHeader(module),
|
|
2859
|
-
type: "action"
|
|
2860
|
-
});
|
|
2861
|
-
}
|
|
2862
|
-
for (const [key, apis] of grouped.functions) {
|
|
2863
|
-
const entityName = key === "global" ? void 0 : key;
|
|
2864
|
-
const declarations = generateActionDeclarations(apis, true, entityName, { importPath });
|
|
2865
|
-
const module = generateActionModule(apis, true, { importPath });
|
|
2866
|
-
allFiles.push({
|
|
2867
|
-
relativePath: `functions/${key}.d.ts`,
|
|
2868
|
-
content: addGeneratedHeader(declarations),
|
|
2869
|
-
type: "action"
|
|
2870
|
-
});
|
|
2871
|
-
allFiles.push({
|
|
2872
|
-
relativePath: `functions/${key}.ts`,
|
|
2873
|
-
content: addGeneratedHeader(module),
|
|
2874
|
-
type: "action"
|
|
2875
|
-
});
|
|
2876
|
-
}
|
|
2877
|
-
this.logger.info(`Generated ${grouped.actions.size} action groups, ${grouped.functions.size} function groups`);
|
|
2878
|
-
} else {
|
|
2879
|
-
this.logger.info("No Custom APIs found");
|
|
2880
|
-
}
|
|
3046
|
+
const actionFiles = await this.generateActions(metadataClient);
|
|
3047
|
+
allFiles.push(...actionFiles);
|
|
2881
3048
|
}
|
|
2882
3049
|
if (this.config.entities.length > 0) {
|
|
2883
3050
|
const entityNamesContent = generateEntityNamesEnum(this.config.entities, {
|
|
@@ -2899,6 +3066,15 @@ var TypeGenerationOrchestrator = class {
|
|
|
2899
3066
|
allFiles.push(indexFile);
|
|
2900
3067
|
}
|
|
2901
3068
|
const writeResult = await writeAllFiles(this.config.outputDir, allFiles);
|
|
3069
|
+
if (deletedEntityNames.length > 0) {
|
|
3070
|
+
const deleted = await deleteOrphanedFiles(this.config.outputDir, deletedEntityNames);
|
|
3071
|
+
if (deleted > 0) {
|
|
3072
|
+
this.logger.info(`Deleted ${deleted} orphaned files for removed entities`);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
if (this.config.useCache && cache) {
|
|
3076
|
+
await this.updateCache(cache, cachedEntityInfos, deletedEntityNames, newVersionStamp);
|
|
3077
|
+
}
|
|
2902
3078
|
const durationMs = Date.now() - startTime;
|
|
2903
3079
|
const entityWarnings = entityResults.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
2904
3080
|
const totalWarnings = entityWarnings + writeResult.warnings.length;
|
|
@@ -2911,22 +3087,129 @@ var TypeGenerationOrchestrator = class {
|
|
|
2911
3087
|
filesUnchanged: writeResult.unchanged,
|
|
2912
3088
|
totalFiles: allFiles.length,
|
|
2913
3089
|
totalWarnings,
|
|
2914
|
-
durationMs
|
|
3090
|
+
durationMs,
|
|
3091
|
+
cacheUsed: cacheStats?.cacheUsed ?? false
|
|
2915
3092
|
});
|
|
2916
3093
|
return {
|
|
2917
3094
|
entities: entityResults,
|
|
2918
3095
|
totalFiles: allFiles.length,
|
|
2919
3096
|
totalWarnings,
|
|
2920
|
-
durationMs
|
|
3097
|
+
durationMs,
|
|
3098
|
+
cacheStats
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Resolve the cache: load existing cache, detect changes, determine which
|
|
3103
|
+
* entities need to be fetched vs. can be served from cache.
|
|
3104
|
+
*
|
|
3105
|
+
* On any failure (corrupt cache, expired stamp), falls back to full refresh.
|
|
3106
|
+
*/
|
|
3107
|
+
async resolveCache(httpClient, requestedEntities) {
|
|
3108
|
+
const cache = new MetadataCache(this.config.cacheDir);
|
|
3109
|
+
const changeDetector = new ChangeDetector(httpClient);
|
|
3110
|
+
let cacheData;
|
|
3111
|
+
try {
|
|
3112
|
+
cacheData = await cache.load(this.config.environmentUrl);
|
|
3113
|
+
} catch {
|
|
3114
|
+
this.logger.warn("Failed to load metadata cache, performing full refresh");
|
|
3115
|
+
cacheData = null;
|
|
3116
|
+
}
|
|
3117
|
+
if (!cacheData || !cacheData.manifest.serverVersionStamp) {
|
|
3118
|
+
this.logger.info("No valid cache found, performing full metadata refresh");
|
|
3119
|
+
const stamp = await changeDetector.getInitialVersionStamp();
|
|
3120
|
+
return {
|
|
3121
|
+
cache,
|
|
3122
|
+
cachedEntities: {},
|
|
3123
|
+
deletedEntityNames: [],
|
|
3124
|
+
newVersionStamp: stamp,
|
|
3125
|
+
stats: {
|
|
3126
|
+
cacheUsed: true,
|
|
3127
|
+
fullRefresh: true,
|
|
3128
|
+
entitiesFromCache: 0,
|
|
3129
|
+
entitiesFetched: requestedEntities.size,
|
|
3130
|
+
entitiesDeleted: 0
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
let changeResult;
|
|
3135
|
+
try {
|
|
3136
|
+
changeResult = await changeDetector.detectChanges(cacheData.manifest.serverVersionStamp);
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
const isExpired = error instanceof Error && error.message.includes("META_3005" /* META_VERSION_STAMP_EXPIRED */);
|
|
3139
|
+
if (isExpired) {
|
|
3140
|
+
this.logger.warn("Cache version stamp expired (>90 days), performing full refresh");
|
|
3141
|
+
} else {
|
|
3142
|
+
this.logger.warn("Change detection failed, performing full refresh", {
|
|
3143
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
const stamp = await changeDetector.getInitialVersionStamp();
|
|
3147
|
+
return {
|
|
3148
|
+
cache,
|
|
3149
|
+
cachedEntities: {},
|
|
3150
|
+
deletedEntityNames: [],
|
|
3151
|
+
newVersionStamp: stamp,
|
|
3152
|
+
stats: {
|
|
3153
|
+
cacheUsed: true,
|
|
3154
|
+
fullRefresh: true,
|
|
3155
|
+
entitiesFromCache: 0,
|
|
3156
|
+
entitiesFetched: requestedEntities.size,
|
|
3157
|
+
entitiesDeleted: 0
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
const changedSet = new Set(changeResult.changedEntityNames);
|
|
3162
|
+
const cachedEntities = {};
|
|
3163
|
+
let entitiesFromCache = 0;
|
|
3164
|
+
let entitiesFetched = 0;
|
|
3165
|
+
for (const entityName of requestedEntities) {
|
|
3166
|
+
if (changedSet.has(entityName) || !cacheData.entityTypeInfos[entityName]) {
|
|
3167
|
+
entitiesFetched++;
|
|
3168
|
+
} else {
|
|
3169
|
+
cachedEntities[entityName] = cacheData.entityTypeInfos[entityName];
|
|
3170
|
+
entitiesFromCache++;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
const deletedEntityNames = changeResult.deletedEntityNames.filter(
|
|
3174
|
+
(name) => requestedEntities.has(name)
|
|
3175
|
+
);
|
|
3176
|
+
this.logger.info(`Cache delta: ${entitiesFromCache} from cache, ${entitiesFetched} to fetch, ${deletedEntityNames.length} deleted`);
|
|
3177
|
+
return {
|
|
3178
|
+
cache,
|
|
3179
|
+
cachedEntities,
|
|
3180
|
+
deletedEntityNames,
|
|
3181
|
+
newVersionStamp: changeResult.newVersionStamp,
|
|
3182
|
+
stats: {
|
|
3183
|
+
cacheUsed: true,
|
|
3184
|
+
fullRefresh: false,
|
|
3185
|
+
entitiesFromCache,
|
|
3186
|
+
entitiesFetched,
|
|
3187
|
+
entitiesDeleted: deletedEntityNames.length
|
|
3188
|
+
}
|
|
2921
3189
|
};
|
|
2922
3190
|
}
|
|
2923
3191
|
/**
|
|
2924
|
-
*
|
|
3192
|
+
* Update the metadata cache after a successful generation run.
|
|
2925
3193
|
*/
|
|
2926
|
-
async
|
|
3194
|
+
async updateCache(cache, entityTypeInfos, deletedEntityNames, newVersionStamp) {
|
|
3195
|
+
try {
|
|
3196
|
+
if (deletedEntityNames.length > 0) {
|
|
3197
|
+
await cache.removeEntities(this.config.environmentUrl, deletedEntityNames, newVersionStamp);
|
|
3198
|
+
}
|
|
3199
|
+
await cache.save(this.config.environmentUrl, entityTypeInfos, newVersionStamp);
|
|
3200
|
+
this.logger.info("Metadata cache updated");
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
this.logger.warn("Failed to update metadata cache", {
|
|
3203
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Generate all output files for a single entity from its metadata.
|
|
3209
|
+
*/
|
|
3210
|
+
generateEntityFiles(entityName, entityInfo) {
|
|
2927
3211
|
const warnings = [];
|
|
2928
3212
|
const files = [];
|
|
2929
|
-
const entityInfo = await metadataClient.getEntityTypeInfo(entityName);
|
|
2930
3213
|
if (this.config.generateEntities) {
|
|
2931
3214
|
const entityContent = generateEntityInterface(entityInfo, {
|
|
2932
3215
|
labelConfig: this.config.labelConfig,
|
|
@@ -2982,6 +3265,58 @@ var TypeGenerationOrchestrator = class {
|
|
|
2982
3265
|
}
|
|
2983
3266
|
return { entityLogicalName: entityName, files, warnings };
|
|
2984
3267
|
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Generate Custom API Action/Function executor files.
|
|
3270
|
+
*/
|
|
3271
|
+
async generateActions(metadataClient) {
|
|
3272
|
+
const files = [];
|
|
3273
|
+
this.logger.info("Fetching Custom APIs...");
|
|
3274
|
+
let customApis = await metadataClient.getCustomApis();
|
|
3275
|
+
if (this.config.actionsFilter) {
|
|
3276
|
+
const prefix = this.config.actionsFilter.toLowerCase();
|
|
3277
|
+
const before = customApis.length;
|
|
3278
|
+
customApis = customApis.filter((api) => api.api.uniquename.toLowerCase().startsWith(prefix));
|
|
3279
|
+
this.logger.info(`Filtered Custom APIs by prefix "${this.config.actionsFilter}": ${before} -> ${customApis.length}`);
|
|
3280
|
+
}
|
|
3281
|
+
if (customApis.length > 0) {
|
|
3282
|
+
const importPath = "@xrmforge/typegen";
|
|
3283
|
+
const grouped = groupCustomApis(customApis);
|
|
3284
|
+
for (const [key, apis] of grouped.actions) {
|
|
3285
|
+
const entityName = key === "global" ? void 0 : key;
|
|
3286
|
+
const declarations = generateActionDeclarations(apis, false, entityName, { importPath });
|
|
3287
|
+
const module = generateActionModule(apis, false, { importPath });
|
|
3288
|
+
files.push({
|
|
3289
|
+
relativePath: `actions/${key}.d.ts`,
|
|
3290
|
+
content: addGeneratedHeader(declarations),
|
|
3291
|
+
type: "action"
|
|
3292
|
+
});
|
|
3293
|
+
files.push({
|
|
3294
|
+
relativePath: `actions/${key}.ts`,
|
|
3295
|
+
content: addGeneratedHeader(module),
|
|
3296
|
+
type: "action"
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
for (const [key, apis] of grouped.functions) {
|
|
3300
|
+
const entityName = key === "global" ? void 0 : key;
|
|
3301
|
+
const declarations = generateActionDeclarations(apis, true, entityName, { importPath });
|
|
3302
|
+
const module = generateActionModule(apis, true, { importPath });
|
|
3303
|
+
files.push({
|
|
3304
|
+
relativePath: `functions/${key}.d.ts`,
|
|
3305
|
+
content: addGeneratedHeader(declarations),
|
|
3306
|
+
type: "action"
|
|
3307
|
+
});
|
|
3308
|
+
files.push({
|
|
3309
|
+
relativePath: `functions/${key}.ts`,
|
|
3310
|
+
content: addGeneratedHeader(module),
|
|
3311
|
+
type: "action"
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
this.logger.info(`Generated ${grouped.actions.size} action groups, ${grouped.functions.size} function groups`);
|
|
3315
|
+
} else {
|
|
3316
|
+
this.logger.info("No Custom APIs found");
|
|
3317
|
+
}
|
|
3318
|
+
return files;
|
|
3319
|
+
}
|
|
2985
3320
|
/**
|
|
2986
3321
|
* Extract picklist attributes with their OptionSet metadata.
|
|
2987
3322
|
* Maps the raw EntityTypeInfo data to the format expected by the OptionSet generator.
|
|
@@ -3016,6 +3351,7 @@ export {
|
|
|
3016
3351
|
ApiRequestError,
|
|
3017
3352
|
AuthenticationError,
|
|
3018
3353
|
BindingType,
|
|
3354
|
+
ChangeDetector,
|
|
3019
3355
|
ClientState,
|
|
3020
3356
|
ClientType,
|
|
3021
3357
|
ConfigError,
|