@xrmforge/typegen 0.2.0 → 0.4.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 +141 -10
- package/dist/index.js +455 -99
- 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 = [];
|
|
@@ -401,6 +404,28 @@ var DataverseHttpClient = class {
|
|
|
401
404
|
}
|
|
402
405
|
return allResults;
|
|
403
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Execute a POST request that is semantically a read operation.
|
|
409
|
+
* Used for Dataverse actions like RetrieveMetadataChanges that require POST
|
|
410
|
+
* but do not modify data. Allowed even in read-only mode.
|
|
411
|
+
*
|
|
412
|
+
* @param path - API path (relative to apiUrl)
|
|
413
|
+
* @param body - JSON body to send
|
|
414
|
+
* @param signal - Optional AbortSignal to cancel the request
|
|
415
|
+
*/
|
|
416
|
+
async postReadOnly(path2, body, signal) {
|
|
417
|
+
const ALLOWED_PATHS = ["/RetrieveMetadataChanges"];
|
|
418
|
+
const normalizedPath = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
419
|
+
if (!ALLOWED_PATHS.some((p) => normalizedPath.startsWith(p))) {
|
|
420
|
+
throw new ApiRequestError(
|
|
421
|
+
"API_2001" /* API_REQUEST_FAILED */,
|
|
422
|
+
`postReadOnly is only allowed for safe read operations: ${ALLOWED_PATHS.join(", ")}. Got: "${normalizedPath}"`,
|
|
423
|
+
{ path: normalizedPath }
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const url = this.resolveUrl(path2);
|
|
427
|
+
return this.executeWithConcurrency(url, signal, "POST", body);
|
|
428
|
+
}
|
|
404
429
|
// ─── Read-Only Enforcement ─────────────────────────────────────────────
|
|
405
430
|
/**
|
|
406
431
|
* Returns true if this client is in read-only mode (the safe default).
|
|
@@ -471,6 +496,18 @@ var DataverseHttpClient = class {
|
|
|
471
496
|
if (this.cachedToken && this.cachedToken.expiresAt - Date.now() > TOKEN_BUFFER_MS) {
|
|
472
497
|
return this.cachedToken.token;
|
|
473
498
|
}
|
|
499
|
+
if (this.pendingTokenRefresh) {
|
|
500
|
+
return this.pendingTokenRefresh;
|
|
501
|
+
}
|
|
502
|
+
this.pendingTokenRefresh = this.refreshToken();
|
|
503
|
+
try {
|
|
504
|
+
return await this.pendingTokenRefresh;
|
|
505
|
+
} finally {
|
|
506
|
+
this.pendingTokenRefresh = null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** Internal: actually acquire a new token from the credential provider. */
|
|
510
|
+
async refreshToken() {
|
|
474
511
|
log2.debug("Requesting new access token");
|
|
475
512
|
const scope = `${this.baseUrl}/.default`;
|
|
476
513
|
let tokenResponse;
|
|
@@ -508,10 +545,10 @@ var DataverseHttpClient = class {
|
|
|
508
545
|
* The semaphore is acquired ONCE per logical request. Retries happen
|
|
509
546
|
* INSIDE the semaphore to avoid the recursive slot exhaustion bug.
|
|
510
547
|
*/
|
|
511
|
-
async executeWithConcurrency(url, signal) {
|
|
548
|
+
async executeWithConcurrency(url, signal, method = "GET", requestBody) {
|
|
512
549
|
await this.acquireSlot();
|
|
513
550
|
try {
|
|
514
|
-
return await this.executeWithRetry(url, 1, 0, signal);
|
|
551
|
+
return await this.executeWithRetry(url, 1, 0, signal, method, requestBody);
|
|
515
552
|
} finally {
|
|
516
553
|
this.releaseSlot();
|
|
517
554
|
}
|
|
@@ -521,10 +558,10 @@ var DataverseHttpClient = class {
|
|
|
521
558
|
this.activeConcurrentRequests++;
|
|
522
559
|
return Promise.resolve();
|
|
523
560
|
}
|
|
524
|
-
return new Promise((
|
|
561
|
+
return new Promise((resolve2) => {
|
|
525
562
|
this.waitQueue.push(() => {
|
|
526
563
|
this.activeConcurrentRequests++;
|
|
527
|
-
|
|
564
|
+
resolve2();
|
|
528
565
|
});
|
|
529
566
|
});
|
|
530
567
|
}
|
|
@@ -534,7 +571,7 @@ var DataverseHttpClient = class {
|
|
|
534
571
|
if (next) next();
|
|
535
572
|
}
|
|
536
573
|
// ─── Retry Logic (runs INSIDE a single concurrency slot) ─────────────────
|
|
537
|
-
async executeWithRetry(url, attempt, rateLimitRetries = 0, signal) {
|
|
574
|
+
async executeWithRetry(url, attempt, rateLimitRetries = 0, signal, method = "GET", requestBody) {
|
|
538
575
|
if (signal?.aborted) {
|
|
539
576
|
throw new ApiRequestError(
|
|
540
577
|
"API_2001" /* API_REQUEST_FAILED */,
|
|
@@ -549,17 +586,21 @@ var DataverseHttpClient = class {
|
|
|
549
586
|
signal?.addEventListener("abort", onUserAbort, { once: true });
|
|
550
587
|
let response;
|
|
551
588
|
try {
|
|
552
|
-
log2.debug(
|
|
589
|
+
log2.debug(`${method} ${url}`, { attempt });
|
|
590
|
+
const headers = {
|
|
591
|
+
Authorization: `Bearer ${token}`,
|
|
592
|
+
"OData-MaxVersion": "4.0",
|
|
593
|
+
"OData-Version": "4.0",
|
|
594
|
+
Accept: "application/json",
|
|
595
|
+
Prefer: 'odata.include-annotations="*"'
|
|
596
|
+
};
|
|
597
|
+
if (method === "POST") {
|
|
598
|
+
headers["Content-Type"] = "application/json";
|
|
599
|
+
}
|
|
553
600
|
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
|
-
},
|
|
601
|
+
method,
|
|
602
|
+
headers,
|
|
603
|
+
body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0,
|
|
563
604
|
signal: controller.signal
|
|
564
605
|
});
|
|
565
606
|
} catch (fetchError) {
|
|
@@ -579,7 +620,7 @@ var DataverseHttpClient = class {
|
|
|
579
620
|
url
|
|
580
621
|
});
|
|
581
622
|
await this.sleep(delay);
|
|
582
|
-
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal);
|
|
623
|
+
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal, method, requestBody);
|
|
583
624
|
}
|
|
584
625
|
throw new ApiRequestError(
|
|
585
626
|
"API_2005" /* API_TIMEOUT */,
|
|
@@ -594,7 +635,7 @@ var DataverseHttpClient = class {
|
|
|
594
635
|
error: fetchError instanceof Error ? fetchError.message : String(fetchError)
|
|
595
636
|
});
|
|
596
637
|
await this.sleep(delay);
|
|
597
|
-
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal);
|
|
638
|
+
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal, method, requestBody);
|
|
598
639
|
}
|
|
599
640
|
throw new ApiRequestError(
|
|
600
641
|
"API_2001" /* API_REQUEST_FAILED */,
|
|
@@ -609,12 +650,12 @@ var DataverseHttpClient = class {
|
|
|
609
650
|
signal?.removeEventListener("abort", onUserAbort);
|
|
610
651
|
}
|
|
611
652
|
if (!response.ok) {
|
|
612
|
-
return this.handleHttpError(response, url, attempt, rateLimitRetries, signal);
|
|
653
|
+
return this.handleHttpError(response, url, attempt, rateLimitRetries, signal, method, requestBody);
|
|
613
654
|
}
|
|
614
|
-
log2.debug(
|
|
655
|
+
log2.debug(`${method} ${url} -> ${response.status}`, { attempt });
|
|
615
656
|
return response.json();
|
|
616
657
|
}
|
|
617
|
-
async handleHttpError(response, url, attempt, rateLimitRetries, signal) {
|
|
658
|
+
async handleHttpError(response, url, attempt, rateLimitRetries, signal, method = "GET", requestBody) {
|
|
618
659
|
const body = await response.text();
|
|
619
660
|
if (response.status === 429) {
|
|
620
661
|
if (rateLimitRetries >= this.maxRateLimitRetries) {
|
|
@@ -631,12 +672,12 @@ var DataverseHttpClient = class {
|
|
|
631
672
|
retryAfterHeader
|
|
632
673
|
});
|
|
633
674
|
await this.sleep(retryAfterMs);
|
|
634
|
-
return this.executeWithRetry(url, attempt, rateLimitRetries + 1, signal);
|
|
675
|
+
return this.executeWithRetry(url, attempt, rateLimitRetries + 1, signal, method, requestBody);
|
|
635
676
|
}
|
|
636
677
|
if (response.status === 401 && attempt === 1) {
|
|
637
678
|
log2.warn("HTTP 401 received, clearing token cache and retrying");
|
|
638
679
|
this.cachedToken = null;
|
|
639
|
-
return this.executeWithRetry(url, attempt + 1, 0, signal);
|
|
680
|
+
return this.executeWithRetry(url, attempt + 1, 0, signal, method, requestBody);
|
|
640
681
|
}
|
|
641
682
|
if (response.status >= 500 && attempt <= this.maxRetries) {
|
|
642
683
|
const delay = this.calculateBackoff(attempt);
|
|
@@ -644,7 +685,7 @@ var DataverseHttpClient = class {
|
|
|
644
685
|
`Server error ${response.status}, retrying in ${delay}ms (${attempt}/${this.maxRetries})`
|
|
645
686
|
);
|
|
646
687
|
await this.sleep(delay);
|
|
647
|
-
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal);
|
|
688
|
+
return this.executeWithRetry(url, attempt + 1, rateLimitRetries, signal, method, requestBody);
|
|
648
689
|
}
|
|
649
690
|
const errorCode = response.status === 401 ? "API_2004" /* API_UNAUTHORIZED */ : response.status === 404 ? "API_2003" /* API_NOT_FOUND */ : "API_2001" /* API_REQUEST_FAILED */;
|
|
650
691
|
throw new ApiRequestError(
|
|
@@ -667,7 +708,7 @@ var DataverseHttpClient = class {
|
|
|
667
708
|
return Math.min(exponential + jitter, MAX_BACKOFF_MS);
|
|
668
709
|
}
|
|
669
710
|
sleep(ms) {
|
|
670
|
-
return new Promise((
|
|
711
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
671
712
|
}
|
|
672
713
|
};
|
|
673
714
|
|
|
@@ -1243,10 +1284,12 @@ var MetadataCache = class {
|
|
|
1243
1284
|
cacheDir;
|
|
1244
1285
|
cacheFilePath;
|
|
1245
1286
|
/**
|
|
1246
|
-
* @param
|
|
1287
|
+
* @param cacheDir - Directory where cache files are stored.
|
|
1288
|
+
* Can be an absolute path or relative to cwd.
|
|
1289
|
+
* Defaults to ".xrmforge/cache" when constructed without argument.
|
|
1247
1290
|
*/
|
|
1248
|
-
constructor(
|
|
1249
|
-
this.cacheDir = path.
|
|
1291
|
+
constructor(cacheDir = CACHE_DIR) {
|
|
1292
|
+
this.cacheDir = path.resolve(cacheDir);
|
|
1250
1293
|
this.cacheFilePath = path.join(this.cacheDir, CACHE_FILE);
|
|
1251
1294
|
}
|
|
1252
1295
|
/**
|
|
@@ -1362,6 +1405,105 @@ var MetadataCache = class {
|
|
|
1362
1405
|
}
|
|
1363
1406
|
};
|
|
1364
1407
|
|
|
1408
|
+
// src/metadata/change-detector.ts
|
|
1409
|
+
var log6 = createLogger("change-detector");
|
|
1410
|
+
var EXPIRED_VERSION_STAMP_ERROR = "0x80044352";
|
|
1411
|
+
var ChangeDetector = class {
|
|
1412
|
+
constructor(http) {
|
|
1413
|
+
this.http = http;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Detect which entities have changed since the given version stamp.
|
|
1417
|
+
*
|
|
1418
|
+
* @param clientVersionStamp - The ServerVersionStamp from the last run (from cache)
|
|
1419
|
+
* @returns Changed entity names, deleted entity names, and new version stamp
|
|
1420
|
+
* @throws {MetadataError} with META_VERSION_STAMP_EXPIRED if stamp is too old (>90 days)
|
|
1421
|
+
*/
|
|
1422
|
+
async detectChanges(clientVersionStamp) {
|
|
1423
|
+
log6.info("Detecting metadata changes since last run");
|
|
1424
|
+
const requestBody = {
|
|
1425
|
+
Query: {
|
|
1426
|
+
Criteria: {
|
|
1427
|
+
FilterOperator: "And",
|
|
1428
|
+
Conditions: []
|
|
1429
|
+
},
|
|
1430
|
+
Properties: {
|
|
1431
|
+
AllProperties: false,
|
|
1432
|
+
PropertyNames: ["LogicalName"]
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
ClientVersionStamp: clientVersionStamp,
|
|
1436
|
+
DeletedMetadataFilters: "Entity"
|
|
1437
|
+
};
|
|
1438
|
+
let response;
|
|
1439
|
+
try {
|
|
1440
|
+
response = await this.http.postReadOnly(
|
|
1441
|
+
"/RetrieveMetadataChanges",
|
|
1442
|
+
requestBody
|
|
1443
|
+
);
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
if (this.isExpiredVersionStampError(error)) {
|
|
1446
|
+
throw new MetadataError(
|
|
1447
|
+
"META_3005" /* META_VERSION_STAMP_EXPIRED */,
|
|
1448
|
+
"Cached version stamp has expired (>90 days). A full metadata refresh is required.",
|
|
1449
|
+
{ clientVersionStamp }
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
const changedEntityNames = (response.EntityMetadata ?? []).filter((e) => e.HasChanged !== false).map((e) => e.LogicalName).filter(Boolean);
|
|
1455
|
+
const deletedEntityNames = [];
|
|
1456
|
+
if (response.DeletedMetadata?.Keys) {
|
|
1457
|
+
log6.info(`${response.DeletedMetadata.Keys.length} entity metadata IDs were deleted`);
|
|
1458
|
+
}
|
|
1459
|
+
const newVersionStamp = response.ServerVersionStamp;
|
|
1460
|
+
log6.info(`Change detection complete: ${changedEntityNames.length} changed, ${deletedEntityNames.length} deleted`, {
|
|
1461
|
+
changedEntityNames: changedEntityNames.length <= 10 ? changedEntityNames : `${changedEntityNames.length} entities`,
|
|
1462
|
+
newVersionStamp: newVersionStamp.substring(0, 20) + "..."
|
|
1463
|
+
});
|
|
1464
|
+
return {
|
|
1465
|
+
changedEntityNames,
|
|
1466
|
+
deletedEntityNames,
|
|
1467
|
+
newVersionStamp
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Perform an initial metadata query to get the first ServerVersionStamp.
|
|
1472
|
+
* This is used on the very first run (no cache exists).
|
|
1473
|
+
*
|
|
1474
|
+
* @returns The initial server version stamp
|
|
1475
|
+
*/
|
|
1476
|
+
async getInitialVersionStamp() {
|
|
1477
|
+
log6.info("Fetching initial server version stamp");
|
|
1478
|
+
const requestBody = {
|
|
1479
|
+
Query: {
|
|
1480
|
+
Criteria: {
|
|
1481
|
+
FilterOperator: "And",
|
|
1482
|
+
Conditions: []
|
|
1483
|
+
},
|
|
1484
|
+
Properties: {
|
|
1485
|
+
AllProperties: false,
|
|
1486
|
+
PropertyNames: ["LogicalName"]
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
const response = await this.http.postReadOnly(
|
|
1491
|
+
"/RetrieveMetadataChanges",
|
|
1492
|
+
requestBody
|
|
1493
|
+
);
|
|
1494
|
+
log6.info("Initial version stamp acquired");
|
|
1495
|
+
return response.ServerVersionStamp;
|
|
1496
|
+
}
|
|
1497
|
+
/** Check if an error is the expired version stamp error (0x80044352) */
|
|
1498
|
+
isExpiredVersionStampError(error) {
|
|
1499
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1500
|
+
const errorRecord = error;
|
|
1501
|
+
const contextBody = errorRecord?.context ? String(errorRecord.context["responseBody"] ?? "") : "";
|
|
1502
|
+
const combined = msg + contextBody;
|
|
1503
|
+
return combined.includes(EXPIRED_VERSION_STAMP_ERROR) || combined.includes("ExpiredVersionStamp");
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1365
1507
|
// src/metadata/labels.ts
|
|
1366
1508
|
var DEFAULT_LABEL_CONFIG = {
|
|
1367
1509
|
primaryLanguage: 1033
|
|
@@ -1455,10 +1597,12 @@ function getLabelLanguagesParam(config) {
|
|
|
1455
1597
|
}
|
|
1456
1598
|
|
|
1457
1599
|
// src/generators/type-mapping.ts
|
|
1600
|
+
var log7 = createLogger("type-mapping");
|
|
1458
1601
|
function getEntityPropertyType(attributeType, isLookup = false) {
|
|
1459
1602
|
if (isLookup) return "string";
|
|
1460
1603
|
const mapping = ENTITY_TYPE_MAP[attributeType];
|
|
1461
1604
|
if (mapping) return mapping;
|
|
1605
|
+
log7.warn(`Unmapped AttributeType "${attributeType}" falling back to "unknown"`);
|
|
1462
1606
|
return "unknown";
|
|
1463
1607
|
}
|
|
1464
1608
|
var ENTITY_TYPE_MAP = {
|
|
@@ -1497,6 +1641,7 @@ var ENTITY_TYPE_MAP = {
|
|
|
1497
1641
|
function getFormAttributeType(attributeType) {
|
|
1498
1642
|
const mapping = FORM_ATTRIBUTE_TYPE_MAP[attributeType];
|
|
1499
1643
|
if (mapping) return mapping;
|
|
1644
|
+
log7.warn(`Unmapped form AttributeType "${attributeType}" falling back to generic Attribute`);
|
|
1500
1645
|
return "Xrm.Attributes.Attribute";
|
|
1501
1646
|
}
|
|
1502
1647
|
var FORM_ATTRIBUTE_TYPE_MAP = {
|
|
@@ -2169,6 +2314,24 @@ function generateEntityNavigationProperties(info, options = {}) {
|
|
|
2169
2314
|
return lines.join("\n");
|
|
2170
2315
|
}
|
|
2171
2316
|
|
|
2317
|
+
// src/generators/entity-names-generator.ts
|
|
2318
|
+
function generateEntityNamesEnum(entityNames, options = {}) {
|
|
2319
|
+
const namespace = options.namespace ?? "XrmForge";
|
|
2320
|
+
const sorted = [...entityNames].sort();
|
|
2321
|
+
const lines = [];
|
|
2322
|
+
lines.push(`declare namespace ${namespace} {`);
|
|
2323
|
+
lines.push(" /** Entity logical names for Xrm.WebApi calls (compile-time only, zero runtime) */");
|
|
2324
|
+
lines.push(" const enum EntityNames {");
|
|
2325
|
+
for (const name of sorted) {
|
|
2326
|
+
const pascal = toPascalCase(name);
|
|
2327
|
+
lines.push(` ${pascal} = '${name}',`);
|
|
2328
|
+
}
|
|
2329
|
+
lines.push(" }");
|
|
2330
|
+
lines.push("}");
|
|
2331
|
+
lines.push("");
|
|
2332
|
+
return lines.join("\n");
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2172
2335
|
// src/generators/webapi-helpers.ts
|
|
2173
2336
|
function select(...fields) {
|
|
2174
2337
|
if (fields.length === 0) return "";
|
|
@@ -2362,6 +2525,7 @@ function createBoundAction(operationName, entityLogicalName, paramMeta) {
|
|
|
2362
2525
|
}
|
|
2363
2526
|
function createUnboundAction(operationName, paramMeta) {
|
|
2364
2527
|
return {
|
|
2528
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- return type varies (Response or parsed JSON)
|
|
2365
2529
|
async execute(params) {
|
|
2366
2530
|
const req = buildUnboundRequest(
|
|
2367
2531
|
operationName,
|
|
@@ -2646,7 +2810,7 @@ function groupCustomApis(apis) {
|
|
|
2646
2810
|
}
|
|
2647
2811
|
|
|
2648
2812
|
// src/orchestrator/file-writer.ts
|
|
2649
|
-
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
2813
|
+
import { mkdir, writeFile, readFile, unlink } from "fs/promises";
|
|
2650
2814
|
import { join as join2, dirname } from "path";
|
|
2651
2815
|
async function writeGeneratedFile(outputDir, file) {
|
|
2652
2816
|
const absolutePath = join2(outputDir, file.relativePath);
|
|
@@ -2678,6 +2842,24 @@ async function writeAllFiles(outputDir, files) {
|
|
|
2678
2842
|
}
|
|
2679
2843
|
return result;
|
|
2680
2844
|
}
|
|
2845
|
+
async function deleteOrphanedFiles(outputDir, deletedEntityNames) {
|
|
2846
|
+
let deleted = 0;
|
|
2847
|
+
const subdirs = ["entities", "optionsets", "forms"];
|
|
2848
|
+
for (const entityName of deletedEntityNames) {
|
|
2849
|
+
for (const subdir of subdirs) {
|
|
2850
|
+
const filePath = join2(outputDir, subdir, `${entityName}.d.ts`);
|
|
2851
|
+
try {
|
|
2852
|
+
await unlink(filePath);
|
|
2853
|
+
deleted++;
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
if (error.code !== "ENOENT") {
|
|
2856
|
+
throw error;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return deleted;
|
|
2862
|
+
}
|
|
2681
2863
|
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
|
|
2682
2864
|
// This file was generated by @xrmforge/typegen. Do not edit manually.
|
|
2683
2865
|
// Re-run 'xrmforge generate' to update.
|
|
@@ -2724,11 +2906,6 @@ var TypeGenerationOrchestrator = class {
|
|
|
2724
2906
|
constructor(credential, config, logger) {
|
|
2725
2907
|
this.credential = credential;
|
|
2726
2908
|
this.logger = logger ?? createLogger("orchestrator");
|
|
2727
|
-
if (config.useCache) {
|
|
2728
|
-
throw new Error(
|
|
2729
|
-
"Metadata caching is not yet implemented (planned for v0.2.0). Remove the useCache option or set it to false."
|
|
2730
|
-
);
|
|
2731
|
-
}
|
|
2732
2909
|
this.config = {
|
|
2733
2910
|
environmentUrl: config.environmentUrl,
|
|
2734
2911
|
entities: [...config.entities],
|
|
@@ -2739,6 +2916,7 @@ var TypeGenerationOrchestrator = class {
|
|
|
2739
2916
|
generateForms: config.generateForms ?? true,
|
|
2740
2917
|
generateOptionSets: config.generateOptionSets ?? true,
|
|
2741
2918
|
generateActions: config.generateActions ?? false,
|
|
2919
|
+
actionsFilter: config.actionsFilter ?? "",
|
|
2742
2920
|
useCache: config.useCache ?? false,
|
|
2743
2921
|
cacheDir: config.cacheDir ?? ".xrmforge/cache",
|
|
2744
2922
|
namespacePrefix: config.namespacePrefix ?? "XrmForge"
|
|
@@ -2762,7 +2940,8 @@ var TypeGenerationOrchestrator = class {
|
|
|
2762
2940
|
}
|
|
2763
2941
|
this.logger.info("Starting type generation", {
|
|
2764
2942
|
entities: this.config.entities,
|
|
2765
|
-
outputDir: this.config.outputDir
|
|
2943
|
+
outputDir: this.config.outputDir,
|
|
2944
|
+
useCache: this.config.useCache
|
|
2766
2945
|
});
|
|
2767
2946
|
const httpClient = new DataverseHttpClient({
|
|
2768
2947
|
environmentUrl: this.config.environmentUrl,
|
|
@@ -2785,74 +2964,81 @@ var TypeGenerationOrchestrator = class {
|
|
|
2785
2964
|
durationMs: Date.now() - startTime
|
|
2786
2965
|
};
|
|
2787
2966
|
}
|
|
2788
|
-
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2967
|
+
let cacheStats;
|
|
2968
|
+
const entitiesToFetch = new Set(this.config.entities);
|
|
2969
|
+
const cachedEntityInfos = {};
|
|
2970
|
+
let cache;
|
|
2971
|
+
let newVersionStamp = null;
|
|
2972
|
+
const deletedEntityNames = [];
|
|
2973
|
+
if (this.config.useCache) {
|
|
2974
|
+
const cacheResult = await this.resolveCache(httpClient, entitiesToFetch);
|
|
2975
|
+
cache = cacheResult.cache;
|
|
2976
|
+
newVersionStamp = cacheResult.newVersionStamp;
|
|
2977
|
+
cacheStats = cacheResult.stats;
|
|
2978
|
+
for (const [name, info] of Object.entries(cacheResult.cachedEntities)) {
|
|
2979
|
+
cachedEntityInfos[name] = info;
|
|
2980
|
+
entitiesToFetch.delete(name);
|
|
2981
|
+
}
|
|
2982
|
+
deletedEntityNames.push(...cacheResult.deletedEntityNames);
|
|
2983
|
+
}
|
|
2984
|
+
const fetchList = [...entitiesToFetch];
|
|
2985
|
+
const failedEntities = /* @__PURE__ */ new Map();
|
|
2986
|
+
if (fetchList.length > 0) {
|
|
2987
|
+
this.logger.info(`Fetching ${fetchList.length} entities from Dataverse`);
|
|
2988
|
+
const settled = await Promise.allSettled(
|
|
2989
|
+
fetchList.map((entityName) => {
|
|
2990
|
+
if (signal?.aborted) {
|
|
2991
|
+
return Promise.reject(new Error("Generation aborted"));
|
|
2992
|
+
}
|
|
2993
|
+
return metadataClient.getEntityTypeInfo(entityName).then((info) => {
|
|
2994
|
+
this.logger.info(`Fetched entity: ${entityName}`);
|
|
2995
|
+
return { entityName, info };
|
|
2996
|
+
});
|
|
2997
|
+
})
|
|
2998
|
+
);
|
|
2999
|
+
for (let i = 0; i < settled.length; i++) {
|
|
3000
|
+
const outcome = settled[i];
|
|
3001
|
+
const entityName = fetchList[i];
|
|
3002
|
+
if (outcome.status === "fulfilled") {
|
|
3003
|
+
cachedEntityInfos[outcome.value.entityName] = outcome.value.info;
|
|
3004
|
+
} else {
|
|
3005
|
+
const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
3006
|
+
this.logger.error(`Failed to fetch entity: ${entityName}`, { error: outcome.reason });
|
|
3007
|
+
failedEntities.set(entityName, errorMsg);
|
|
2793
3008
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
for (let i = 0; i < settled.length; i++) {
|
|
2801
|
-
const outcome = settled[i];
|
|
2802
|
-
const entityName = this.config.entities[i];
|
|
2803
|
-
if (outcome.status === "fulfilled") {
|
|
2804
|
-
entityResults.push(outcome.value);
|
|
2805
|
-
allFiles.push(...outcome.value.files);
|
|
2806
|
-
} else {
|
|
2807
|
-
const errorMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
2808
|
-
this.logger.error(`Failed to process entity: ${entityName}`, { error: outcome.reason });
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
this.logger.info(`Generating types for ${this.config.entities.length - failedEntities.size} entities`);
|
|
3012
|
+
for (const entityName of this.config.entities) {
|
|
3013
|
+
if (signal?.aborted) break;
|
|
3014
|
+
if (failedEntities.has(entityName)) {
|
|
2809
3015
|
entityResults.push({
|
|
2810
3016
|
entityLogicalName: entityName,
|
|
2811
3017
|
files: [],
|
|
2812
|
-
warnings: [`Failed to process: ${
|
|
3018
|
+
warnings: [`Failed to process: ${failedEntities.get(entityName)}`]
|
|
2813
3019
|
});
|
|
3020
|
+
continue;
|
|
2814
3021
|
}
|
|
3022
|
+
const entityInfo = cachedEntityInfos[entityName];
|
|
3023
|
+
if (!entityInfo) continue;
|
|
3024
|
+
const result = this.generateEntityFiles(entityName, entityInfo);
|
|
3025
|
+
this.logger.info(`Generated entity: ${entityName} (${result.files.length} files)`);
|
|
3026
|
+
entityResults.push(result);
|
|
3027
|
+
allFiles.push(...result.files);
|
|
2815
3028
|
}
|
|
2816
3029
|
if (this.config.generateActions && !signal?.aborted) {
|
|
2817
|
-
this.
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
type: "action"
|
|
2830
|
-
});
|
|
2831
|
-
allFiles.push({
|
|
2832
|
-
relativePath: `actions/${key}.ts`,
|
|
2833
|
-
content: addGeneratedHeader(module),
|
|
2834
|
-
type: "action"
|
|
2835
|
-
});
|
|
2836
|
-
}
|
|
2837
|
-
for (const [key, apis] of grouped.functions) {
|
|
2838
|
-
const entityName = key === "global" ? void 0 : key;
|
|
2839
|
-
const declarations = generateActionDeclarations(apis, true, entityName, { importPath });
|
|
2840
|
-
const module = generateActionModule(apis, true, { importPath });
|
|
2841
|
-
allFiles.push({
|
|
2842
|
-
relativePath: `functions/${key}.d.ts`,
|
|
2843
|
-
content: addGeneratedHeader(declarations),
|
|
2844
|
-
type: "action"
|
|
2845
|
-
});
|
|
2846
|
-
allFiles.push({
|
|
2847
|
-
relativePath: `functions/${key}.ts`,
|
|
2848
|
-
content: addGeneratedHeader(module),
|
|
2849
|
-
type: "action"
|
|
2850
|
-
});
|
|
2851
|
-
}
|
|
2852
|
-
this.logger.info(`Generated ${grouped.actions.size} action groups, ${grouped.functions.size} function groups`);
|
|
2853
|
-
} else {
|
|
2854
|
-
this.logger.info("No Custom APIs found");
|
|
2855
|
-
}
|
|
3030
|
+
const actionFiles = await this.generateActions(metadataClient);
|
|
3031
|
+
allFiles.push(...actionFiles);
|
|
3032
|
+
}
|
|
3033
|
+
if (this.config.entities.length > 0) {
|
|
3034
|
+
const entityNamesContent = generateEntityNamesEnum(this.config.entities, {
|
|
3035
|
+
namespace: this.config.namespacePrefix
|
|
3036
|
+
});
|
|
3037
|
+
allFiles.push({
|
|
3038
|
+
relativePath: "entity-names.d.ts",
|
|
3039
|
+
content: addGeneratedHeader(entityNamesContent),
|
|
3040
|
+
type: "entity"
|
|
3041
|
+
});
|
|
2856
3042
|
}
|
|
2857
3043
|
if (allFiles.length > 0) {
|
|
2858
3044
|
const indexContent = generateBarrelIndex(allFiles);
|
|
@@ -2864,6 +3050,15 @@ var TypeGenerationOrchestrator = class {
|
|
|
2864
3050
|
allFiles.push(indexFile);
|
|
2865
3051
|
}
|
|
2866
3052
|
const writeResult = await writeAllFiles(this.config.outputDir, allFiles);
|
|
3053
|
+
if (deletedEntityNames.length > 0) {
|
|
3054
|
+
const deleted = await deleteOrphanedFiles(this.config.outputDir, deletedEntityNames);
|
|
3055
|
+
if (deleted > 0) {
|
|
3056
|
+
this.logger.info(`Deleted ${deleted} orphaned files for removed entities`);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
if (this.config.useCache && cache) {
|
|
3060
|
+
await this.updateCache(cache, cachedEntityInfos, deletedEntityNames, newVersionStamp);
|
|
3061
|
+
}
|
|
2867
3062
|
const durationMs = Date.now() - startTime;
|
|
2868
3063
|
const entityWarnings = entityResults.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
2869
3064
|
const totalWarnings = entityWarnings + writeResult.warnings.length;
|
|
@@ -2876,22 +3071,129 @@ var TypeGenerationOrchestrator = class {
|
|
|
2876
3071
|
filesUnchanged: writeResult.unchanged,
|
|
2877
3072
|
totalFiles: allFiles.length,
|
|
2878
3073
|
totalWarnings,
|
|
2879
|
-
durationMs
|
|
3074
|
+
durationMs,
|
|
3075
|
+
cacheUsed: cacheStats?.cacheUsed ?? false
|
|
2880
3076
|
});
|
|
2881
3077
|
return {
|
|
2882
3078
|
entities: entityResults,
|
|
2883
3079
|
totalFiles: allFiles.length,
|
|
2884
3080
|
totalWarnings,
|
|
2885
|
-
durationMs
|
|
3081
|
+
durationMs,
|
|
3082
|
+
cacheStats
|
|
2886
3083
|
};
|
|
2887
3084
|
}
|
|
2888
3085
|
/**
|
|
2889
|
-
*
|
|
3086
|
+
* Resolve the cache: load existing cache, detect changes, determine which
|
|
3087
|
+
* entities need to be fetched vs. can be served from cache.
|
|
3088
|
+
*
|
|
3089
|
+
* On any failure (corrupt cache, expired stamp), falls back to full refresh.
|
|
2890
3090
|
*/
|
|
2891
|
-
async
|
|
3091
|
+
async resolveCache(httpClient, requestedEntities) {
|
|
3092
|
+
const cache = new MetadataCache(this.config.cacheDir);
|
|
3093
|
+
const changeDetector = new ChangeDetector(httpClient);
|
|
3094
|
+
let cacheData;
|
|
3095
|
+
try {
|
|
3096
|
+
cacheData = await cache.load(this.config.environmentUrl);
|
|
3097
|
+
} catch {
|
|
3098
|
+
this.logger.warn("Failed to load metadata cache, performing full refresh");
|
|
3099
|
+
cacheData = null;
|
|
3100
|
+
}
|
|
3101
|
+
if (!cacheData || !cacheData.manifest.serverVersionStamp) {
|
|
3102
|
+
this.logger.info("No valid cache found, performing full metadata refresh");
|
|
3103
|
+
const stamp = await changeDetector.getInitialVersionStamp();
|
|
3104
|
+
return {
|
|
3105
|
+
cache,
|
|
3106
|
+
cachedEntities: {},
|
|
3107
|
+
deletedEntityNames: [],
|
|
3108
|
+
newVersionStamp: stamp,
|
|
3109
|
+
stats: {
|
|
3110
|
+
cacheUsed: true,
|
|
3111
|
+
fullRefresh: true,
|
|
3112
|
+
entitiesFromCache: 0,
|
|
3113
|
+
entitiesFetched: requestedEntities.size,
|
|
3114
|
+
entitiesDeleted: 0
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
let changeResult;
|
|
3119
|
+
try {
|
|
3120
|
+
changeResult = await changeDetector.detectChanges(cacheData.manifest.serverVersionStamp);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
const isExpired = error instanceof Error && error.message.includes("META_3005" /* META_VERSION_STAMP_EXPIRED */);
|
|
3123
|
+
if (isExpired) {
|
|
3124
|
+
this.logger.warn("Cache version stamp expired (>90 days), performing full refresh");
|
|
3125
|
+
} else {
|
|
3126
|
+
this.logger.warn("Change detection failed, performing full refresh", {
|
|
3127
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
const stamp = await changeDetector.getInitialVersionStamp();
|
|
3131
|
+
return {
|
|
3132
|
+
cache,
|
|
3133
|
+
cachedEntities: {},
|
|
3134
|
+
deletedEntityNames: [],
|
|
3135
|
+
newVersionStamp: stamp,
|
|
3136
|
+
stats: {
|
|
3137
|
+
cacheUsed: true,
|
|
3138
|
+
fullRefresh: true,
|
|
3139
|
+
entitiesFromCache: 0,
|
|
3140
|
+
entitiesFetched: requestedEntities.size,
|
|
3141
|
+
entitiesDeleted: 0
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
const changedSet = new Set(changeResult.changedEntityNames);
|
|
3146
|
+
const cachedEntities = {};
|
|
3147
|
+
let entitiesFromCache = 0;
|
|
3148
|
+
let entitiesFetched = 0;
|
|
3149
|
+
for (const entityName of requestedEntities) {
|
|
3150
|
+
if (changedSet.has(entityName) || !cacheData.entityTypeInfos[entityName]) {
|
|
3151
|
+
entitiesFetched++;
|
|
3152
|
+
} else {
|
|
3153
|
+
cachedEntities[entityName] = cacheData.entityTypeInfos[entityName];
|
|
3154
|
+
entitiesFromCache++;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
const deletedEntityNames = changeResult.deletedEntityNames.filter(
|
|
3158
|
+
(name) => requestedEntities.has(name)
|
|
3159
|
+
);
|
|
3160
|
+
this.logger.info(`Cache delta: ${entitiesFromCache} from cache, ${entitiesFetched} to fetch, ${deletedEntityNames.length} deleted`);
|
|
3161
|
+
return {
|
|
3162
|
+
cache,
|
|
3163
|
+
cachedEntities,
|
|
3164
|
+
deletedEntityNames,
|
|
3165
|
+
newVersionStamp: changeResult.newVersionStamp,
|
|
3166
|
+
stats: {
|
|
3167
|
+
cacheUsed: true,
|
|
3168
|
+
fullRefresh: false,
|
|
3169
|
+
entitiesFromCache,
|
|
3170
|
+
entitiesFetched,
|
|
3171
|
+
entitiesDeleted: deletedEntityNames.length
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Update the metadata cache after a successful generation run.
|
|
3177
|
+
*/
|
|
3178
|
+
async updateCache(cache, entityTypeInfos, deletedEntityNames, newVersionStamp) {
|
|
3179
|
+
try {
|
|
3180
|
+
if (deletedEntityNames.length > 0) {
|
|
3181
|
+
await cache.removeEntities(this.config.environmentUrl, deletedEntityNames, newVersionStamp);
|
|
3182
|
+
}
|
|
3183
|
+
await cache.save(this.config.environmentUrl, entityTypeInfos, newVersionStamp);
|
|
3184
|
+
this.logger.info("Metadata cache updated");
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
this.logger.warn("Failed to update metadata cache", {
|
|
3187
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Generate all output files for a single entity from its metadata.
|
|
3193
|
+
*/
|
|
3194
|
+
generateEntityFiles(entityName, entityInfo) {
|
|
2892
3195
|
const warnings = [];
|
|
2893
3196
|
const files = [];
|
|
2894
|
-
const entityInfo = await metadataClient.getEntityTypeInfo(entityName);
|
|
2895
3197
|
if (this.config.generateEntities) {
|
|
2896
3198
|
const entityContent = generateEntityInterface(entityInfo, {
|
|
2897
3199
|
labelConfig: this.config.labelConfig,
|
|
@@ -2947,6 +3249,58 @@ var TypeGenerationOrchestrator = class {
|
|
|
2947
3249
|
}
|
|
2948
3250
|
return { entityLogicalName: entityName, files, warnings };
|
|
2949
3251
|
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Generate Custom API Action/Function executor files.
|
|
3254
|
+
*/
|
|
3255
|
+
async generateActions(metadataClient) {
|
|
3256
|
+
const files = [];
|
|
3257
|
+
this.logger.info("Fetching Custom APIs...");
|
|
3258
|
+
let customApis = await metadataClient.getCustomApis();
|
|
3259
|
+
if (this.config.actionsFilter) {
|
|
3260
|
+
const prefix = this.config.actionsFilter.toLowerCase();
|
|
3261
|
+
const before = customApis.length;
|
|
3262
|
+
customApis = customApis.filter((api) => api.api.uniquename.toLowerCase().startsWith(prefix));
|
|
3263
|
+
this.logger.info(`Filtered Custom APIs by prefix "${this.config.actionsFilter}": ${before} -> ${customApis.length}`);
|
|
3264
|
+
}
|
|
3265
|
+
if (customApis.length > 0) {
|
|
3266
|
+
const importPath = "@xrmforge/typegen";
|
|
3267
|
+
const grouped = groupCustomApis(customApis);
|
|
3268
|
+
for (const [key, apis] of grouped.actions) {
|
|
3269
|
+
const entityName = key === "global" ? void 0 : key;
|
|
3270
|
+
const declarations = generateActionDeclarations(apis, false, entityName, { importPath });
|
|
3271
|
+
const module = generateActionModule(apis, false, { importPath });
|
|
3272
|
+
files.push({
|
|
3273
|
+
relativePath: `actions/${key}.d.ts`,
|
|
3274
|
+
content: addGeneratedHeader(declarations),
|
|
3275
|
+
type: "action"
|
|
3276
|
+
});
|
|
3277
|
+
files.push({
|
|
3278
|
+
relativePath: `actions/${key}.ts`,
|
|
3279
|
+
content: addGeneratedHeader(module),
|
|
3280
|
+
type: "action"
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
for (const [key, apis] of grouped.functions) {
|
|
3284
|
+
const entityName = key === "global" ? void 0 : key;
|
|
3285
|
+
const declarations = generateActionDeclarations(apis, true, entityName, { importPath });
|
|
3286
|
+
const module = generateActionModule(apis, true, { importPath });
|
|
3287
|
+
files.push({
|
|
3288
|
+
relativePath: `functions/${key}.d.ts`,
|
|
3289
|
+
content: addGeneratedHeader(declarations),
|
|
3290
|
+
type: "action"
|
|
3291
|
+
});
|
|
3292
|
+
files.push({
|
|
3293
|
+
relativePath: `functions/${key}.ts`,
|
|
3294
|
+
content: addGeneratedHeader(module),
|
|
3295
|
+
type: "action"
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
this.logger.info(`Generated ${grouped.actions.size} action groups, ${grouped.functions.size} function groups`);
|
|
3299
|
+
} else {
|
|
3300
|
+
this.logger.info("No Custom APIs found");
|
|
3301
|
+
}
|
|
3302
|
+
return files;
|
|
3303
|
+
}
|
|
2950
3304
|
/**
|
|
2951
3305
|
* Extract picklist attributes with their OptionSet metadata.
|
|
2952
3306
|
* Maps the raw EntityTypeInfo data to the format expected by the OptionSet generator.
|
|
@@ -2981,6 +3335,7 @@ export {
|
|
|
2981
3335
|
ApiRequestError,
|
|
2982
3336
|
AuthenticationError,
|
|
2983
3337
|
BindingType,
|
|
3338
|
+
ChangeDetector,
|
|
2984
3339
|
ClientState,
|
|
2985
3340
|
ClientType,
|
|
2986
3341
|
ConfigError,
|
|
@@ -3025,6 +3380,7 @@ export {
|
|
|
3025
3380
|
generateEntityFieldsEnum,
|
|
3026
3381
|
generateEntityForms,
|
|
3027
3382
|
generateEntityInterface,
|
|
3383
|
+
generateEntityNamesEnum,
|
|
3028
3384
|
generateEntityNavigationProperties,
|
|
3029
3385
|
generateEntityOptionSets,
|
|
3030
3386
|
generateEnumMembers,
|