@xylex-group/athena 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -1
- package/bin/athena-js.js +7 -8
- package/dist/cli/index.cjs +2502 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +6 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +2500 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +1554 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +484 -1
- package/dist/index.d.ts +484 -1
- package/dist/index.js +1536 -16
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve, dirname, posix } from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
6
|
+
|
|
1
7
|
// src/gateway/errors.ts
|
|
2
8
|
var AthenaGatewayError = class _AthenaGatewayError extends Error {
|
|
3
9
|
code;
|
|
@@ -364,6 +370,137 @@ function createAthenaGatewayClient(config = {}) {
|
|
|
364
370
|
};
|
|
365
371
|
}
|
|
366
372
|
|
|
373
|
+
// src/sql-identifiers.ts
|
|
374
|
+
var SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
375
|
+
var COMPOSITE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
376
|
+
var ALIAS_PATTERN = /^([A-Za-z_][A-Za-z0-9_.]*)\s+(?:as\s+)?([A-Za-z_][A-Za-z0-9_]*)$/i;
|
|
377
|
+
function quoteIdentifierSegment(identifier2) {
|
|
378
|
+
return `"${identifier2.replace(/"/g, '""')}"`;
|
|
379
|
+
}
|
|
380
|
+
function quoteQualifiedIdentifier(identifier2) {
|
|
381
|
+
return identifier2.split(".").map((segment) => quoteIdentifierSegment(segment)).join(".");
|
|
382
|
+
}
|
|
383
|
+
function quoteSelectToken(token) {
|
|
384
|
+
if (token === "*") return token;
|
|
385
|
+
if (COMPOSITE_IDENTIFIER_PATTERN.test(token)) {
|
|
386
|
+
return quoteQualifiedIdentifier(token);
|
|
387
|
+
}
|
|
388
|
+
const aliasMatch = ALIAS_PATTERN.exec(token);
|
|
389
|
+
if (!aliasMatch) {
|
|
390
|
+
return token;
|
|
391
|
+
}
|
|
392
|
+
const [, baseIdentifier, aliasIdentifier] = aliasMatch;
|
|
393
|
+
if (!COMPOSITE_IDENTIFIER_PATTERN.test(baseIdentifier) || !SIMPLE_IDENTIFIER_PATTERN.test(aliasIdentifier)) {
|
|
394
|
+
return token;
|
|
395
|
+
}
|
|
396
|
+
return `${quoteQualifiedIdentifier(baseIdentifier)} AS ${quoteIdentifierSegment(aliasIdentifier)}`;
|
|
397
|
+
}
|
|
398
|
+
function canAutoQuoteToken(token) {
|
|
399
|
+
if (token === "*") return true;
|
|
400
|
+
if (COMPOSITE_IDENTIFIER_PATTERN.test(token)) return true;
|
|
401
|
+
const aliasMatch = ALIAS_PATTERN.exec(token);
|
|
402
|
+
if (!aliasMatch) return false;
|
|
403
|
+
const [, baseIdentifier, aliasIdentifier] = aliasMatch;
|
|
404
|
+
return COMPOSITE_IDENTIFIER_PATTERN.test(baseIdentifier) && SIMPLE_IDENTIFIER_PATTERN.test(aliasIdentifier);
|
|
405
|
+
}
|
|
406
|
+
function splitTopLevelCommaSeparated(input) {
|
|
407
|
+
const parts = [];
|
|
408
|
+
let buffer = "";
|
|
409
|
+
let singleQuoted = false;
|
|
410
|
+
let doubleQuoted = false;
|
|
411
|
+
let depth = 0;
|
|
412
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
413
|
+
const char = input[index];
|
|
414
|
+
const next = index + 1 < input.length ? input[index + 1] : "";
|
|
415
|
+
if (singleQuoted) {
|
|
416
|
+
buffer += char;
|
|
417
|
+
if (char === "'" && next === "'") {
|
|
418
|
+
buffer += next;
|
|
419
|
+
index += 1;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (char === "'") {
|
|
423
|
+
singleQuoted = false;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (doubleQuoted) {
|
|
428
|
+
buffer += char;
|
|
429
|
+
if (char === '"' && next === '"') {
|
|
430
|
+
buffer += next;
|
|
431
|
+
index += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (char === '"') {
|
|
435
|
+
doubleQuoted = false;
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (char === "'") {
|
|
440
|
+
singleQuoted = true;
|
|
441
|
+
buffer += char;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (char === '"') {
|
|
445
|
+
doubleQuoted = true;
|
|
446
|
+
buffer += char;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (char === "(") {
|
|
450
|
+
depth += 1;
|
|
451
|
+
buffer += char;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (char === ")") {
|
|
455
|
+
depth -= 1;
|
|
456
|
+
if (depth < 0) return null;
|
|
457
|
+
buffer += char;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (char === "," && depth === 0) {
|
|
461
|
+
parts.push(buffer.trim());
|
|
462
|
+
buffer = "";
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
buffer += char;
|
|
466
|
+
}
|
|
467
|
+
if (singleQuoted || doubleQuoted || depth !== 0) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
if (buffer.trim().length > 0) {
|
|
471
|
+
parts.push(buffer.trim());
|
|
472
|
+
}
|
|
473
|
+
return parts;
|
|
474
|
+
}
|
|
475
|
+
function quoteSelectColumnsExpression(columns) {
|
|
476
|
+
const trimmed = columns.trim();
|
|
477
|
+
if (!trimmed || trimmed === "*") return trimmed || "*";
|
|
478
|
+
const tokens = splitTopLevelCommaSeparated(trimmed);
|
|
479
|
+
if (!tokens || tokens.length === 0) {
|
|
480
|
+
return trimmed;
|
|
481
|
+
}
|
|
482
|
+
if (!tokens.every(canAutoQuoteToken)) {
|
|
483
|
+
return trimmed;
|
|
484
|
+
}
|
|
485
|
+
return tokens.map(quoteSelectToken).join(", ");
|
|
486
|
+
}
|
|
487
|
+
var SqlIdentifierPath = class {
|
|
488
|
+
segments;
|
|
489
|
+
constructor(segments) {
|
|
490
|
+
this.segments = segments;
|
|
491
|
+
}
|
|
492
|
+
toSql() {
|
|
493
|
+
return this.segments.map(quoteIdentifierSegment).join(".");
|
|
494
|
+
}
|
|
495
|
+
toString() {
|
|
496
|
+
return this.toSql();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
function identifier(...segments) {
|
|
500
|
+
const expandedSegments = segments.flatMap((segment) => segment.split(".")).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
501
|
+
return new SqlIdentifierPath(expandedSegments);
|
|
502
|
+
}
|
|
503
|
+
|
|
367
504
|
// src/client.ts
|
|
368
505
|
var DEFAULT_COLUMNS = "*";
|
|
369
506
|
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -467,12 +604,6 @@ function normalizeCast(cast) {
|
|
|
467
604
|
function escapeSqlStringLiteral(value) {
|
|
468
605
|
return value.replace(/'/g, "''");
|
|
469
606
|
}
|
|
470
|
-
function quoteIdentifier(identifier) {
|
|
471
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
472
|
-
}
|
|
473
|
-
function quoteQualifiedIdentifier(identifier) {
|
|
474
|
-
return identifier.split(".").map((segment) => quoteIdentifier(segment)).join(".");
|
|
475
|
-
}
|
|
476
607
|
function toSqlLiteral(value) {
|
|
477
608
|
if (value === null) return "NULL";
|
|
478
609
|
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
|
|
@@ -487,7 +618,7 @@ function buildSelectColumnsClause(columns) {
|
|
|
487
618
|
if (Array.isArray(columns)) {
|
|
488
619
|
return columns.map((column) => quoteQualifiedIdentifier(column)).join(", ");
|
|
489
620
|
}
|
|
490
|
-
return columns;
|
|
621
|
+
return quoteSelectColumnsExpression(columns);
|
|
491
622
|
}
|
|
492
623
|
function conditionToSqlClause(condition) {
|
|
493
624
|
if (!condition.column) return null;
|
|
@@ -1172,6 +1303,56 @@ var Backend = {
|
|
|
1172
1303
|
};
|
|
1173
1304
|
|
|
1174
1305
|
// src/auxiliaries.ts
|
|
1306
|
+
var AthenaErrorKind = {
|
|
1307
|
+
UniqueViolation: "unique_violation",
|
|
1308
|
+
NotFound: "not_found",
|
|
1309
|
+
Validation: "validation",
|
|
1310
|
+
Auth: "auth",
|
|
1311
|
+
RateLimit: "rate_limit",
|
|
1312
|
+
Transient: "transient",
|
|
1313
|
+
Unknown: "unknown"
|
|
1314
|
+
};
|
|
1315
|
+
var AthenaErrorCode = {
|
|
1316
|
+
UniqueViolation: "UNIQUE_VIOLATION",
|
|
1317
|
+
NotFound: "NOT_FOUND",
|
|
1318
|
+
ValidationFailed: "VALIDATION_FAILED",
|
|
1319
|
+
AuthUnauthorized: "AUTH_UNAUTHORIZED",
|
|
1320
|
+
AuthForbidden: "AUTH_FORBIDDEN",
|
|
1321
|
+
RateLimited: "RATE_LIMITED",
|
|
1322
|
+
NetworkUnavailable: "NETWORK_UNAVAILABLE",
|
|
1323
|
+
TransientFailure: "TRANSIENT_FAILURE",
|
|
1324
|
+
HttpFailure: "HTTP_FAILURE",
|
|
1325
|
+
Unknown: "UNKNOWN"
|
|
1326
|
+
};
|
|
1327
|
+
var AthenaErrorCategory = {
|
|
1328
|
+
Transport: "transport",
|
|
1329
|
+
Client: "client",
|
|
1330
|
+
Server: "server",
|
|
1331
|
+
Database: "database",
|
|
1332
|
+
Unknown: "unknown"
|
|
1333
|
+
};
|
|
1334
|
+
var AthenaError = class extends Error {
|
|
1335
|
+
code;
|
|
1336
|
+
kind;
|
|
1337
|
+
category;
|
|
1338
|
+
status;
|
|
1339
|
+
retryable;
|
|
1340
|
+
requestId;
|
|
1341
|
+
context;
|
|
1342
|
+
raw;
|
|
1343
|
+
constructor(input) {
|
|
1344
|
+
super(input.message);
|
|
1345
|
+
this.name = "AthenaError";
|
|
1346
|
+
this.code = input.code;
|
|
1347
|
+
this.kind = input.kind;
|
|
1348
|
+
this.category = input.category;
|
|
1349
|
+
this.status = input.status;
|
|
1350
|
+
this.retryable = input.retryable ?? false;
|
|
1351
|
+
this.requestId = input.requestId;
|
|
1352
|
+
this.context = input.context;
|
|
1353
|
+
this.raw = input.raw;
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1175
1356
|
function isRecord2(value) {
|
|
1176
1357
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1177
1358
|
}
|
|
@@ -1237,6 +1418,44 @@ function classifyKind(status, code, message) {
|
|
|
1237
1418
|
}
|
|
1238
1419
|
return "unknown";
|
|
1239
1420
|
}
|
|
1421
|
+
function toAthenaErrorCode(kind, status, gatewayCode) {
|
|
1422
|
+
if (gatewayCode === "NETWORK_ERROR" || kind === "transient" && status === 0) {
|
|
1423
|
+
return AthenaErrorCode.NetworkUnavailable;
|
|
1424
|
+
}
|
|
1425
|
+
switch (kind) {
|
|
1426
|
+
case "unique_violation":
|
|
1427
|
+
return AthenaErrorCode.UniqueViolation;
|
|
1428
|
+
case "not_found":
|
|
1429
|
+
return AthenaErrorCode.NotFound;
|
|
1430
|
+
case "validation":
|
|
1431
|
+
return AthenaErrorCode.ValidationFailed;
|
|
1432
|
+
case "rate_limit":
|
|
1433
|
+
return AthenaErrorCode.RateLimited;
|
|
1434
|
+
case "auth":
|
|
1435
|
+
if (status === 403) {
|
|
1436
|
+
return AthenaErrorCode.AuthForbidden;
|
|
1437
|
+
}
|
|
1438
|
+
return AthenaErrorCode.AuthUnauthorized;
|
|
1439
|
+
case "transient":
|
|
1440
|
+
return status !== void 0 && status >= 500 ? AthenaErrorCode.HttpFailure : AthenaErrorCode.TransientFailure;
|
|
1441
|
+
case "unknown":
|
|
1442
|
+
default:
|
|
1443
|
+
return status !== void 0 && status >= 400 ? AthenaErrorCode.HttpFailure : AthenaErrorCode.Unknown;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function toAthenaErrorCategory(kind, status) {
|
|
1447
|
+
if (kind === "transient" && (status === 0 || status === void 0)) {
|
|
1448
|
+
return AthenaErrorCategory.Transport;
|
|
1449
|
+
}
|
|
1450
|
+
if (kind === "unique_violation") return AthenaErrorCategory.Database;
|
|
1451
|
+
if (kind === "validation" || kind === "auth" || kind === "not_found") return AthenaErrorCategory.Client;
|
|
1452
|
+
if (kind === "rate_limit" || kind === "transient") return AthenaErrorCategory.Server;
|
|
1453
|
+
return AthenaErrorCategory.Unknown;
|
|
1454
|
+
}
|
|
1455
|
+
function isRetryable(kind, status) {
|
|
1456
|
+
if (kind === "rate_limit" || kind === "transient") return true;
|
|
1457
|
+
return status !== void 0 && status >= 500;
|
|
1458
|
+
}
|
|
1240
1459
|
function toGatewayCode(kind, status) {
|
|
1241
1460
|
if (kind === "transient" && (status === 0 || status === void 0)) return "NETWORK_ERROR";
|
|
1242
1461
|
if (kind === "validation") return "INVALID_JSON";
|
|
@@ -1279,9 +1498,14 @@ function normalizeAthenaError(resultOrError, context) {
|
|
|
1279
1498
|
const operation = context?.operation ?? operationFromDetails(details);
|
|
1280
1499
|
const table = context?.table ?? extractTable(message2);
|
|
1281
1500
|
const constraint = extractConstraint(message2);
|
|
1282
|
-
const
|
|
1501
|
+
const kind2 = classifyKind(resultOrError.status, details?.code, message2);
|
|
1502
|
+
const code = toAthenaErrorCode(kind2, resultOrError.status, details?.code);
|
|
1503
|
+
const category = toAthenaErrorCategory(kind2, resultOrError.status);
|
|
1283
1504
|
return {
|
|
1284
|
-
kind,
|
|
1505
|
+
kind: kind2,
|
|
1506
|
+
code,
|
|
1507
|
+
category,
|
|
1508
|
+
retryable: isRetryable(kind2, resultOrError.status),
|
|
1285
1509
|
status: resultOrError.status,
|
|
1286
1510
|
constraint,
|
|
1287
1511
|
table,
|
|
@@ -1295,9 +1519,14 @@ function normalizeAthenaError(resultOrError, context) {
|
|
|
1295
1519
|
const operation = context?.operation ?? operationFromDetails(details);
|
|
1296
1520
|
const table = context?.table ?? extractTable(resultOrError.message);
|
|
1297
1521
|
const constraint = extractConstraint(resultOrError.message);
|
|
1298
|
-
const
|
|
1522
|
+
const kind2 = classifyKind(resultOrError.status, resultOrError.code, resultOrError.message);
|
|
1523
|
+
const code = toAthenaErrorCode(kind2, resultOrError.status, resultOrError.code);
|
|
1524
|
+
const category = toAthenaErrorCategory(kind2, resultOrError.status);
|
|
1299
1525
|
return {
|
|
1300
|
-
kind,
|
|
1526
|
+
kind: kind2,
|
|
1527
|
+
code,
|
|
1528
|
+
category,
|
|
1529
|
+
retryable: isRetryable(kind2, resultOrError.status),
|
|
1301
1530
|
status: resultOrError.status,
|
|
1302
1531
|
constraint,
|
|
1303
1532
|
table,
|
|
@@ -1308,8 +1537,12 @@ function normalizeAthenaError(resultOrError, context) {
|
|
|
1308
1537
|
}
|
|
1309
1538
|
if (resultOrError instanceof Error) {
|
|
1310
1539
|
const maybeStatus = isRecord2(resultOrError) && typeof resultOrError.status === "number" ? resultOrError.status : void 0;
|
|
1540
|
+
const kind2 = classifyKind(maybeStatus, void 0, resultOrError.message);
|
|
1311
1541
|
return {
|
|
1312
|
-
kind:
|
|
1542
|
+
kind: kind2,
|
|
1543
|
+
code: toAthenaErrorCode(kind2, maybeStatus),
|
|
1544
|
+
category: toAthenaErrorCategory(kind2, maybeStatus),
|
|
1545
|
+
retryable: isRetryable(kind2, maybeStatus),
|
|
1313
1546
|
status: maybeStatus,
|
|
1314
1547
|
constraint: extractConstraint(resultOrError.message),
|
|
1315
1548
|
table: context?.table ?? extractTable(resultOrError.message),
|
|
@@ -1319,8 +1552,12 @@ function normalizeAthenaError(resultOrError, context) {
|
|
|
1319
1552
|
};
|
|
1320
1553
|
}
|
|
1321
1554
|
const message = typeof resultOrError === "string" ? resultOrError : "Unknown Athena error";
|
|
1555
|
+
const kind = classifyKind(void 0, void 0, message);
|
|
1322
1556
|
return {
|
|
1323
|
-
kind
|
|
1557
|
+
kind,
|
|
1558
|
+
code: toAthenaErrorCode(kind, void 0),
|
|
1559
|
+
category: toAthenaErrorCategory(kind, void 0),
|
|
1560
|
+
retryable: isRetryable(kind, void 0),
|
|
1324
1561
|
status: void 0,
|
|
1325
1562
|
constraint: extractConstraint(message),
|
|
1326
1563
|
table: context?.table ?? extractTable(message),
|
|
@@ -1453,8 +1690,8 @@ function computeDelayMs(attempt, error, config) {
|
|
|
1453
1690
|
}
|
|
1454
1691
|
function sleep(ms) {
|
|
1455
1692
|
if (ms <= 0) return Promise.resolve();
|
|
1456
|
-
return new Promise((
|
|
1457
|
-
setTimeout(
|
|
1693
|
+
return new Promise((resolve3) => {
|
|
1694
|
+
setTimeout(resolve3, ms);
|
|
1458
1695
|
});
|
|
1459
1696
|
}
|
|
1460
1697
|
async function withRetry(config, fn) {
|
|
@@ -1485,6 +1722,1289 @@ async function withRetry(config, fn) {
|
|
|
1485
1722
|
throw new Error("withRetry reached an unexpected state");
|
|
1486
1723
|
}
|
|
1487
1724
|
|
|
1488
|
-
|
|
1725
|
+
// src/schema/definitions.ts
|
|
1726
|
+
function defineModel(input) {
|
|
1727
|
+
return input;
|
|
1728
|
+
}
|
|
1729
|
+
function defineSchema(models) {
|
|
1730
|
+
return { models };
|
|
1731
|
+
}
|
|
1732
|
+
function defineDatabase(schemas) {
|
|
1733
|
+
return { schemas };
|
|
1734
|
+
}
|
|
1735
|
+
function defineRegistry(databases) {
|
|
1736
|
+
return databases;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/schema/typed-client.ts
|
|
1740
|
+
var TenantHeaderMapper = class {
|
|
1741
|
+
constructor(tenantKeyMap) {
|
|
1742
|
+
this.tenantKeyMap = tenantKeyMap;
|
|
1743
|
+
}
|
|
1744
|
+
apply(baseHeaders, tenantContext) {
|
|
1745
|
+
const headers = {
|
|
1746
|
+
...baseHeaders ?? {}
|
|
1747
|
+
};
|
|
1748
|
+
for (const [tenantKey, headerName] of Object.entries(this.tenantKeyMap)) {
|
|
1749
|
+
const tenantValue = tenantContext[tenantKey];
|
|
1750
|
+
if (tenantValue === void 0 || tenantValue === null) {
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
headers[headerName] = String(tenantValue);
|
|
1754
|
+
}
|
|
1755
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
var RegistryNavigator = class {
|
|
1759
|
+
constructor(registry) {
|
|
1760
|
+
this.registry = registry;
|
|
1761
|
+
}
|
|
1762
|
+
resolveModel(database, schema, model) {
|
|
1763
|
+
const databaseDef = this.registry[database];
|
|
1764
|
+
if (!databaseDef) {
|
|
1765
|
+
throw new Error(`Unknown database "${database}"`);
|
|
1766
|
+
}
|
|
1767
|
+
const schemaDef = databaseDef.schemas[schema];
|
|
1768
|
+
if (!schemaDef) {
|
|
1769
|
+
throw new Error(`Unknown schema "${schema}" in database "${database}"`);
|
|
1770
|
+
}
|
|
1771
|
+
const modelDef = schemaDef.models[model];
|
|
1772
|
+
if (!modelDef) {
|
|
1773
|
+
throw new Error(`Unknown model "${model}" in schema "${schema}"`);
|
|
1774
|
+
}
|
|
1775
|
+
return modelDef;
|
|
1776
|
+
}
|
|
1777
|
+
resolveTableName(schema, model, modelDef) {
|
|
1778
|
+
if (modelDef.meta.tableName) {
|
|
1779
|
+
return modelDef.meta.tableName;
|
|
1780
|
+
}
|
|
1781
|
+
const schemaName = modelDef.meta.schema ?? schema;
|
|
1782
|
+
const modelName = modelDef.meta.model ?? model;
|
|
1783
|
+
return `${schemaName}.${modelName}`;
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
var TypedAthenaClientImpl = class _TypedAthenaClientImpl {
|
|
1787
|
+
registry;
|
|
1788
|
+
tenantKeyMap;
|
|
1789
|
+
tenantContext;
|
|
1790
|
+
baseClient;
|
|
1791
|
+
registryNavigator;
|
|
1792
|
+
tenantHeaderMapper;
|
|
1793
|
+
clientOptions;
|
|
1794
|
+
url;
|
|
1795
|
+
apiKey;
|
|
1796
|
+
constructor(input) {
|
|
1797
|
+
this.registry = input.registry;
|
|
1798
|
+
this.url = input.url;
|
|
1799
|
+
this.apiKey = input.apiKey;
|
|
1800
|
+
const tenantKeyMap = input.options?.tenantKeyMap ?? {};
|
|
1801
|
+
const tenantContext = input.options?.tenantContext ?? {};
|
|
1802
|
+
this.tenantKeyMap = tenantKeyMap;
|
|
1803
|
+
this.tenantContext = tenantContext;
|
|
1804
|
+
this.tenantHeaderMapper = new TenantHeaderMapper(tenantKeyMap);
|
|
1805
|
+
this.registryNavigator = new RegistryNavigator(input.registry);
|
|
1806
|
+
this.clientOptions = {
|
|
1807
|
+
backend: input.options?.backend,
|
|
1808
|
+
client: input.options?.client,
|
|
1809
|
+
headers: input.options?.headers
|
|
1810
|
+
};
|
|
1811
|
+
this.baseClient = createClient(this.url, this.apiKey, {
|
|
1812
|
+
backend: this.clientOptions.backend,
|
|
1813
|
+
client: this.clientOptions.client,
|
|
1814
|
+
headers: this.tenantHeaderMapper.apply(this.clientOptions.headers, tenantContext)
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
from(table) {
|
|
1818
|
+
return this.baseClient.from(table);
|
|
1819
|
+
}
|
|
1820
|
+
rpc(fn, args, options) {
|
|
1821
|
+
return this.baseClient.rpc(fn, args, options);
|
|
1822
|
+
}
|
|
1823
|
+
query(query, options) {
|
|
1824
|
+
return this.baseClient.query(query, options);
|
|
1825
|
+
}
|
|
1826
|
+
withTenantContext(context) {
|
|
1827
|
+
return new _TypedAthenaClientImpl({
|
|
1828
|
+
registry: this.registry,
|
|
1829
|
+
url: this.url,
|
|
1830
|
+
apiKey: this.apiKey,
|
|
1831
|
+
options: {
|
|
1832
|
+
...this.clientOptions,
|
|
1833
|
+
tenantKeyMap: this.tenantKeyMap,
|
|
1834
|
+
tenantContext: {
|
|
1835
|
+
...this.tenantContext,
|
|
1836
|
+
...context ?? {}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
fromModel(database, schema, model) {
|
|
1842
|
+
const modelDef = this.registryNavigator.resolveModel(database, schema, model);
|
|
1843
|
+
const tableName = this.registryNavigator.resolveTableName(schema, model, modelDef);
|
|
1844
|
+
return this.baseClient.from(tableName);
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
function createTypedClient(registry, url, apiKey, options) {
|
|
1848
|
+
return new TypedAthenaClientImpl({
|
|
1849
|
+
registry,
|
|
1850
|
+
url,
|
|
1851
|
+
apiKey,
|
|
1852
|
+
options
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/schema/postgres-introspection-core.ts
|
|
1857
|
+
var POSTGRES_CATALOG_SQL = {
|
|
1858
|
+
columns: `
|
|
1859
|
+
SELECT
|
|
1860
|
+
n.nspname AS schema_name,
|
|
1861
|
+
c.relname AS table_name,
|
|
1862
|
+
a.attname AS column_name,
|
|
1863
|
+
format_type(a.atttypid, a.atttypmod) AS data_type,
|
|
1864
|
+
t.typname AS udt_name,
|
|
1865
|
+
t.typtype AS type_kind_code,
|
|
1866
|
+
t.oid AS type_oid,
|
|
1867
|
+
NOT a.attnotnull AS is_nullable,
|
|
1868
|
+
(ad.adbin IS NOT NULL) AS has_default,
|
|
1869
|
+
(a.attgenerated <> '') AS is_generated,
|
|
1870
|
+
a.attndims AS array_dimensions
|
|
1871
|
+
FROM pg_attribute a
|
|
1872
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
1873
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
1874
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
1875
|
+
LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
|
|
1876
|
+
WHERE c.relkind IN ('r', 'p')
|
|
1877
|
+
AND a.attnum > 0
|
|
1878
|
+
AND NOT a.attisdropped
|
|
1879
|
+
AND n.nspname = ANY($1::text[])
|
|
1880
|
+
ORDER BY n.nspname, c.relname, a.attnum;
|
|
1881
|
+
`,
|
|
1882
|
+
enums: `
|
|
1883
|
+
SELECT
|
|
1884
|
+
t.oid AS type_oid,
|
|
1885
|
+
e.enumlabel AS enum_label
|
|
1886
|
+
FROM pg_type t
|
|
1887
|
+
JOIN pg_enum e ON e.enumtypid = t.oid
|
|
1888
|
+
ORDER BY t.oid, e.enumsortorder;
|
|
1889
|
+
`,
|
|
1890
|
+
primaryKeys: `
|
|
1891
|
+
SELECT
|
|
1892
|
+
n.nspname AS schema_name,
|
|
1893
|
+
c.relname AS table_name,
|
|
1894
|
+
ARRAY_AGG(a.attname ORDER BY ck.ordinality) AS columns
|
|
1895
|
+
FROM pg_constraint con
|
|
1896
|
+
JOIN pg_class c ON c.oid = con.conrelid
|
|
1897
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
1898
|
+
JOIN unnest(con.conkey) WITH ORDINALITY AS ck(attnum, ordinality) ON TRUE
|
|
1899
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ck.attnum
|
|
1900
|
+
WHERE con.contype = 'p'
|
|
1901
|
+
AND n.nspname = ANY($1::text[])
|
|
1902
|
+
GROUP BY n.nspname, c.relname
|
|
1903
|
+
ORDER BY n.nspname, c.relname;
|
|
1904
|
+
`,
|
|
1905
|
+
foreignKeys: `
|
|
1906
|
+
SELECT
|
|
1907
|
+
sn.nspname AS source_schema,
|
|
1908
|
+
sc.relname AS source_table,
|
|
1909
|
+
con.conname AS constraint_name,
|
|
1910
|
+
ARRAY_AGG(sa.attname ORDER BY cols.ordinality) AS source_columns,
|
|
1911
|
+
tn.nspname AS target_schema,
|
|
1912
|
+
tc.relname AS target_table,
|
|
1913
|
+
ARRAY_AGG(ta.attname ORDER BY cols.ordinality) AS target_columns,
|
|
1914
|
+
EXISTS (
|
|
1915
|
+
SELECT 1
|
|
1916
|
+
FROM pg_constraint uq
|
|
1917
|
+
WHERE uq.conrelid = con.conrelid
|
|
1918
|
+
AND uq.contype IN ('p', 'u')
|
|
1919
|
+
AND uq.conkey = con.conkey
|
|
1920
|
+
) AS source_is_unique
|
|
1921
|
+
FROM pg_constraint con
|
|
1922
|
+
JOIN pg_class sc ON sc.oid = con.conrelid
|
|
1923
|
+
JOIN pg_namespace sn ON sn.oid = sc.relnamespace
|
|
1924
|
+
JOIN pg_class tc ON tc.oid = con.confrelid
|
|
1925
|
+
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
|
|
1926
|
+
JOIN unnest(con.conkey, con.confkey) WITH ORDINALITY AS cols(source_attnum, target_attnum, ordinality) ON TRUE
|
|
1927
|
+
JOIN pg_attribute sa ON sa.attrelid = con.conrelid AND sa.attnum = cols.source_attnum
|
|
1928
|
+
JOIN pg_attribute ta ON ta.attrelid = con.confrelid AND ta.attnum = cols.target_attnum
|
|
1929
|
+
WHERE con.contype = 'f'
|
|
1930
|
+
AND sn.nspname = ANY($1::text[])
|
|
1931
|
+
AND tn.nspname = ANY($1::text[])
|
|
1932
|
+
GROUP BY
|
|
1933
|
+
sn.nspname,
|
|
1934
|
+
sc.relname,
|
|
1935
|
+
con.conname,
|
|
1936
|
+
tn.nspname,
|
|
1937
|
+
tc.relname,
|
|
1938
|
+
con.conkey,
|
|
1939
|
+
con.conrelid
|
|
1940
|
+
ORDER BY sn.nspname, sc.relname, con.conname;
|
|
1941
|
+
`
|
|
1942
|
+
};
|
|
1943
|
+
function tableKey(schema, table) {
|
|
1944
|
+
return `${schema}.${table}`;
|
|
1945
|
+
}
|
|
1946
|
+
function relationKey(...parts) {
|
|
1947
|
+
const base = parts.join("_").replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1948
|
+
return base.length > 0 ? base : "relation";
|
|
1949
|
+
}
|
|
1950
|
+
function toTypeKind(code) {
|
|
1951
|
+
switch (code) {
|
|
1952
|
+
case "e":
|
|
1953
|
+
return "enum";
|
|
1954
|
+
case "d":
|
|
1955
|
+
return "domain";
|
|
1956
|
+
case "r":
|
|
1957
|
+
return "range";
|
|
1958
|
+
case "m":
|
|
1959
|
+
return "multirange";
|
|
1960
|
+
case "c":
|
|
1961
|
+
return "composite";
|
|
1962
|
+
default:
|
|
1963
|
+
return "scalar";
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function escapeSqlLiteral(value) {
|
|
1967
|
+
return value.replace(/'/g, "''");
|
|
1968
|
+
}
|
|
1969
|
+
function buildSchemaArrayLiteral(schemas) {
|
|
1970
|
+
const normalized = schemas.length > 0 ? schemas : ["public"];
|
|
1971
|
+
const literals = normalized.map((schema) => `'${escapeSqlLiteral(schema)}'`).join(", ");
|
|
1972
|
+
return `ARRAY[${literals}]`;
|
|
1973
|
+
}
|
|
1974
|
+
function inlineSchemaLiteral(sql, schemas) {
|
|
1975
|
+
const schemaArray = `${buildSchemaArrayLiteral(schemas)}::text[]`;
|
|
1976
|
+
return sql.replace(/\$1::text\[\]/g, schemaArray);
|
|
1977
|
+
}
|
|
1978
|
+
function buildGatewayCatalogQueries(schemas) {
|
|
1979
|
+
return {
|
|
1980
|
+
columns: inlineSchemaLiteral(POSTGRES_CATALOG_SQL.columns, schemas),
|
|
1981
|
+
enums: POSTGRES_CATALOG_SQL.enums,
|
|
1982
|
+
primaryKeys: inlineSchemaLiteral(POSTGRES_CATALOG_SQL.primaryKeys, schemas),
|
|
1983
|
+
foreignKeys: inlineSchemaLiteral(POSTGRES_CATALOG_SQL.foreignKeys, schemas)
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
var PostgresCatalogSnapshotAssembler = class {
|
|
1987
|
+
schemas = {};
|
|
1988
|
+
addColumnRows(columnRows, enumMap) {
|
|
1989
|
+
for (const row of columnRows) {
|
|
1990
|
+
const table = this.ensureTable(row.schema_name, row.table_name);
|
|
1991
|
+
table.columns[row.column_name] = {
|
|
1992
|
+
name: row.column_name,
|
|
1993
|
+
dataType: row.data_type,
|
|
1994
|
+
udtName: row.udt_name,
|
|
1995
|
+
typeKind: toTypeKind(row.type_kind_code),
|
|
1996
|
+
isNullable: row.is_nullable,
|
|
1997
|
+
isPrimaryKey: false,
|
|
1998
|
+
hasDefault: row.has_default,
|
|
1999
|
+
isGenerated: row.is_generated,
|
|
2000
|
+
arrayDimensions: row.array_dimensions ?? 0,
|
|
2001
|
+
enumValues: enumMap.get(row.type_oid)
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
addPrimaryKeyRows(primaryKeyRows) {
|
|
2006
|
+
for (const row of primaryKeyRows) {
|
|
2007
|
+
const table = this.ensureTable(row.schema_name, row.table_name);
|
|
2008
|
+
table.primaryKey = row.columns;
|
|
2009
|
+
for (const columnName of row.columns) {
|
|
2010
|
+
const column = table.columns[columnName];
|
|
2011
|
+
if (column) {
|
|
2012
|
+
column.isPrimaryKey = true;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
addForeignKeyRows(foreignKeyRows) {
|
|
2018
|
+
for (const row of foreignKeyRows) {
|
|
2019
|
+
const sourceTable = this.ensureTable(row.source_schema, row.source_table);
|
|
2020
|
+
const targetTable = this.ensureTable(row.target_schema, row.target_table);
|
|
2021
|
+
const sourceRelationKind = row.source_is_unique ? "one-to-one" : "many-to-one";
|
|
2022
|
+
this.upsertRelation(sourceTable, relationKey(row.constraint_name, row.target_table), {
|
|
2023
|
+
name: row.constraint_name,
|
|
2024
|
+
kind: sourceRelationKind,
|
|
2025
|
+
sourceColumns: row.source_columns,
|
|
2026
|
+
targetSchema: row.target_schema,
|
|
2027
|
+
targetModel: row.target_table,
|
|
2028
|
+
targetColumns: row.target_columns
|
|
2029
|
+
});
|
|
2030
|
+
const targetRelationKind = row.source_is_unique ? "one-to-one" : "one-to-many";
|
|
2031
|
+
this.upsertRelation(targetTable, relationKey(row.source_table), {
|
|
2032
|
+
name: relationKey(row.source_table, row.constraint_name),
|
|
2033
|
+
kind: targetRelationKind,
|
|
2034
|
+
sourceColumns: row.target_columns,
|
|
2035
|
+
targetSchema: row.source_schema,
|
|
2036
|
+
targetModel: row.source_table,
|
|
2037
|
+
targetColumns: row.source_columns
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
addManyToManyRows(foreignKeyRows) {
|
|
2042
|
+
const bySourceTable = /* @__PURE__ */ new Map();
|
|
2043
|
+
for (const fk of foreignKeyRows) {
|
|
2044
|
+
const key = tableKey(fk.source_schema, fk.source_table);
|
|
2045
|
+
const current = bySourceTable.get(key) ?? {
|
|
2046
|
+
schema: fk.source_schema,
|
|
2047
|
+
table: fk.source_table,
|
|
2048
|
+
foreignKeys: []
|
|
2049
|
+
};
|
|
2050
|
+
current.foreignKeys.push(fk);
|
|
2051
|
+
bySourceTable.set(key, current);
|
|
2052
|
+
}
|
|
2053
|
+
for (const candidate of bySourceTable.values()) {
|
|
2054
|
+
const bridgeTable = this.schemas[candidate.schema]?.tables[candidate.table];
|
|
2055
|
+
if (!bridgeTable) continue;
|
|
2056
|
+
const primaryKey = bridgeTable.primaryKey;
|
|
2057
|
+
if (candidate.foreignKeys.length !== 2 || primaryKey.length === 0) continue;
|
|
2058
|
+
const combinedForeignColumns = Array.from(
|
|
2059
|
+
new Set(candidate.foreignKeys.flatMap((fk) => fk.source_columns))
|
|
2060
|
+
);
|
|
2061
|
+
if (combinedForeignColumns.length !== primaryKey.length || !primaryKey.every((column) => combinedForeignColumns.includes(column))) {
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
const [first, second] = candidate.foreignKeys;
|
|
2065
|
+
const firstTarget = this.schemas[first.target_schema]?.tables[first.target_table];
|
|
2066
|
+
const secondTarget = this.schemas[second.target_schema]?.tables[second.target_table];
|
|
2067
|
+
if (!firstTarget || !secondTarget) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
this.upsertRelation(firstTarget, relationKey(second.target_table), {
|
|
2071
|
+
name: relationKey(candidate.table, first.constraint_name, second.constraint_name),
|
|
2072
|
+
kind: "many-to-many",
|
|
2073
|
+
sourceColumns: first.target_columns,
|
|
2074
|
+
targetSchema: second.target_schema,
|
|
2075
|
+
targetModel: second.target_table,
|
|
2076
|
+
targetColumns: second.target_columns,
|
|
2077
|
+
through: {
|
|
2078
|
+
schema: candidate.schema,
|
|
2079
|
+
model: candidate.table,
|
|
2080
|
+
sourceColumns: first.source_columns,
|
|
2081
|
+
targetColumns: second.source_columns
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
this.upsertRelation(secondTarget, relationKey(first.target_table), {
|
|
2085
|
+
name: relationKey(candidate.table, second.constraint_name, first.constraint_name),
|
|
2086
|
+
kind: "many-to-many",
|
|
2087
|
+
sourceColumns: second.target_columns,
|
|
2088
|
+
targetSchema: first.target_schema,
|
|
2089
|
+
targetModel: first.target_table,
|
|
2090
|
+
targetColumns: first.target_columns,
|
|
2091
|
+
through: {
|
|
2092
|
+
schema: candidate.schema,
|
|
2093
|
+
model: candidate.table,
|
|
2094
|
+
sourceColumns: second.source_columns,
|
|
2095
|
+
targetColumns: first.source_columns
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
toSchemas() {
|
|
2101
|
+
return this.schemas;
|
|
2102
|
+
}
|
|
2103
|
+
ensureTable(schemaName, tableName) {
|
|
2104
|
+
if (!this.schemas[schemaName]) {
|
|
2105
|
+
this.schemas[schemaName] = {
|
|
2106
|
+
name: schemaName,
|
|
2107
|
+
tables: {}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const schema = this.schemas[schemaName];
|
|
2111
|
+
if (!schema.tables[tableName]) {
|
|
2112
|
+
schema.tables[tableName] = {
|
|
2113
|
+
schema: schemaName,
|
|
2114
|
+
name: tableName,
|
|
2115
|
+
columns: {},
|
|
2116
|
+
primaryKey: [],
|
|
2117
|
+
relations: {}
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
return schema.tables[tableName];
|
|
2121
|
+
}
|
|
2122
|
+
upsertRelation(table, baseKey, relation) {
|
|
2123
|
+
let key = baseKey;
|
|
2124
|
+
let suffix = 2;
|
|
2125
|
+
while (table.relations[key]) {
|
|
2126
|
+
key = `${baseKey}_${suffix}`;
|
|
2127
|
+
suffix += 1;
|
|
2128
|
+
}
|
|
2129
|
+
table.relations[key] = relation;
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
// src/schema/postgres-provider.ts
|
|
2134
|
+
var PgCatalogClient = class {
|
|
2135
|
+
constructor(pool) {
|
|
2136
|
+
this.pool = pool;
|
|
2137
|
+
}
|
|
2138
|
+
async queryColumns(schemas) {
|
|
2139
|
+
const result = await this.pool.query(POSTGRES_CATALOG_SQL.columns, [schemas]);
|
|
2140
|
+
return result.rows;
|
|
2141
|
+
}
|
|
2142
|
+
async queryEnums() {
|
|
2143
|
+
const result = await this.pool.query(POSTGRES_CATALOG_SQL.enums);
|
|
2144
|
+
const enumMap = /* @__PURE__ */ new Map();
|
|
2145
|
+
for (const row of result.rows) {
|
|
2146
|
+
const existing = enumMap.get(row.type_oid) ?? [];
|
|
2147
|
+
existing.push(row.enum_label);
|
|
2148
|
+
enumMap.set(row.type_oid, existing);
|
|
2149
|
+
}
|
|
2150
|
+
return enumMap;
|
|
2151
|
+
}
|
|
2152
|
+
async queryPrimaryKeys(schemas) {
|
|
2153
|
+
const result = await this.pool.query(POSTGRES_CATALOG_SQL.primaryKeys, [schemas]);
|
|
2154
|
+
return result.rows;
|
|
2155
|
+
}
|
|
2156
|
+
async queryForeignKeys(schemas) {
|
|
2157
|
+
const result = await this.pool.query(POSTGRES_CATALOG_SQL.foreignKeys, [schemas]);
|
|
2158
|
+
return result.rows;
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
var PostgresIntrospectionProvider = class {
|
|
2162
|
+
backend = "postgresql";
|
|
2163
|
+
connectionString;
|
|
2164
|
+
database;
|
|
2165
|
+
constructor(options) {
|
|
2166
|
+
this.connectionString = options.connectionString;
|
|
2167
|
+
this.database = options.database ?? "postgres";
|
|
2168
|
+
}
|
|
2169
|
+
async inspect(options) {
|
|
2170
|
+
const schemas = options?.schemas && options.schemas.length > 0 ? options.schemas : ["public"];
|
|
2171
|
+
const pool = new Pool({
|
|
2172
|
+
connectionString: this.connectionString
|
|
2173
|
+
});
|
|
2174
|
+
const catalogClient = new PgCatalogClient(pool);
|
|
2175
|
+
try {
|
|
2176
|
+
const [columnRows, enumMap, primaryKeyRows, foreignKeyRows] = await Promise.all([
|
|
2177
|
+
catalogClient.queryColumns(schemas),
|
|
2178
|
+
catalogClient.queryEnums(),
|
|
2179
|
+
catalogClient.queryPrimaryKeys(schemas),
|
|
2180
|
+
catalogClient.queryForeignKeys(schemas)
|
|
2181
|
+
]);
|
|
2182
|
+
const assembler = new PostgresCatalogSnapshotAssembler();
|
|
2183
|
+
assembler.addColumnRows(columnRows, enumMap);
|
|
2184
|
+
assembler.addPrimaryKeyRows(primaryKeyRows);
|
|
2185
|
+
assembler.addForeignKeyRows(foreignKeyRows);
|
|
2186
|
+
assembler.addManyToManyRows(foreignKeyRows);
|
|
2187
|
+
return {
|
|
2188
|
+
backend: "postgresql",
|
|
2189
|
+
database: this.database,
|
|
2190
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2191
|
+
schemas: assembler.toSchemas()
|
|
2192
|
+
};
|
|
2193
|
+
} finally {
|
|
2194
|
+
await pool.end();
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
function createPostgresIntrospectionProvider(options) {
|
|
2199
|
+
return new PostgresIntrospectionProvider(options);
|
|
2200
|
+
}
|
|
2201
|
+
var DEFAULT_CONFIG_CANDIDATES = [
|
|
2202
|
+
"athena.config.ts",
|
|
2203
|
+
"athena.config.js",
|
|
2204
|
+
"athena-js.config.ts",
|
|
2205
|
+
"athena-js.config.js",
|
|
2206
|
+
".athena.config.ts",
|
|
2207
|
+
".athena.config.js"
|
|
2208
|
+
];
|
|
2209
|
+
var DEFAULT_TARGETS = {
|
|
2210
|
+
model: "src/generated/{database_kebab}/{schema_kebab}/{model_kebab}.model.ts",
|
|
2211
|
+
schema: "src/generated/{database_kebab}/{schema_kebab}/index.ts",
|
|
2212
|
+
database: "src/generated/{database_kebab}/index.ts",
|
|
2213
|
+
registry: "src/generated/index.ts"
|
|
2214
|
+
};
|
|
2215
|
+
var DEFAULT_NAMING = {
|
|
2216
|
+
modelType: "pascal",
|
|
2217
|
+
modelConst: "camel",
|
|
2218
|
+
schemaConst: "camel",
|
|
2219
|
+
databaseConst: "camel",
|
|
2220
|
+
registryConst: "camel"
|
|
2221
|
+
};
|
|
2222
|
+
var DEFAULT_FEATURES = {
|
|
2223
|
+
emitRelations: true,
|
|
2224
|
+
emitRegistry: true
|
|
2225
|
+
};
|
|
2226
|
+
var DEFAULT_EXPERIMENTAL_FLAGS = {
|
|
2227
|
+
postgresGatewayIntrospection: false,
|
|
2228
|
+
scyllaProviderContracts: true
|
|
2229
|
+
};
|
|
2230
|
+
function normalizeOutputConfig(output) {
|
|
2231
|
+
return {
|
|
2232
|
+
targets: {
|
|
2233
|
+
...DEFAULT_TARGETS,
|
|
2234
|
+
...output.targets ?? {}
|
|
2235
|
+
},
|
|
2236
|
+
placeholderMap: {
|
|
2237
|
+
...output.placeholderMap ?? {}
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
function isAthenaGeneratorConfig(value) {
|
|
2242
|
+
if (!value || typeof value !== "object") {
|
|
2243
|
+
return false;
|
|
2244
|
+
}
|
|
2245
|
+
const record = value;
|
|
2246
|
+
return Boolean(record.provider && typeof record.provider === "object") && Boolean(record.output && typeof record.output === "object");
|
|
2247
|
+
}
|
|
2248
|
+
function normalizeGeneratorConfig(input) {
|
|
2249
|
+
return {
|
|
2250
|
+
provider: input.provider,
|
|
2251
|
+
output: normalizeOutputConfig(input.output),
|
|
2252
|
+
naming: {
|
|
2253
|
+
...DEFAULT_NAMING,
|
|
2254
|
+
...input.naming ?? {}
|
|
2255
|
+
},
|
|
2256
|
+
features: {
|
|
2257
|
+
...DEFAULT_FEATURES,
|
|
2258
|
+
...input.features ?? {}
|
|
2259
|
+
},
|
|
2260
|
+
experimental: {
|
|
2261
|
+
...DEFAULT_EXPERIMENTAL_FLAGS,
|
|
2262
|
+
...input.experimental ?? {}
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function defineGeneratorConfig(config) {
|
|
2267
|
+
return config;
|
|
2268
|
+
}
|
|
2269
|
+
function findGeneratorConfigPath(cwd = process.cwd()) {
|
|
2270
|
+
for (const candidate of DEFAULT_CONFIG_CANDIDATES) {
|
|
2271
|
+
const absolutePath = resolve(cwd, candidate);
|
|
2272
|
+
if (existsSync(absolutePath)) {
|
|
2273
|
+
return absolutePath;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return void 0;
|
|
2277
|
+
}
|
|
2278
|
+
function extractConfigExport(module) {
|
|
2279
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2280
|
+
const queue = [module];
|
|
2281
|
+
while (queue.length > 0) {
|
|
2282
|
+
const current = queue.shift();
|
|
2283
|
+
if (!current || typeof current !== "object" || visited.has(current)) {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
visited.add(current);
|
|
2287
|
+
const record = current;
|
|
2288
|
+
if (isAthenaGeneratorConfig(record)) {
|
|
2289
|
+
return record;
|
|
2290
|
+
}
|
|
2291
|
+
const defaultExport = record.default;
|
|
2292
|
+
if (defaultExport && typeof defaultExport === "object") {
|
|
2293
|
+
queue.push(defaultExport);
|
|
2294
|
+
}
|
|
2295
|
+
const namedConfigExport = record.config;
|
|
2296
|
+
if (namedConfigExport && typeof namedConfigExport === "object") {
|
|
2297
|
+
queue.push(namedConfigExport);
|
|
2298
|
+
}
|
|
2299
|
+
const moduleExports = record["module.exports"];
|
|
2300
|
+
if (moduleExports && typeof moduleExports === "object") {
|
|
2301
|
+
queue.push(moduleExports);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
throw new Error(
|
|
2305
|
+
"Generator config file must export a config object as default export or `config`."
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
function importConfigModule(moduleSpecifier) {
|
|
2309
|
+
const runtimeImport = new Function(
|
|
2310
|
+
"moduleSpecifier",
|
|
2311
|
+
"return import(moduleSpecifier)"
|
|
2312
|
+
);
|
|
2313
|
+
return runtimeImport(moduleSpecifier);
|
|
2314
|
+
}
|
|
2315
|
+
async function loadGeneratorConfig(options = {}) {
|
|
2316
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2317
|
+
const resolvedPath = options.configPath ? resolve(cwd, options.configPath) : findGeneratorConfigPath(cwd);
|
|
2318
|
+
if (!resolvedPath) {
|
|
2319
|
+
throw new Error(
|
|
2320
|
+
`No generator config found in ${cwd}. Expected one of: ${DEFAULT_CONFIG_CANDIDATES.join(", ")}`
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
const moduleUrl = pathToFileURL(resolvedPath);
|
|
2324
|
+
const module = await importConfigModule(`${moduleUrl.href}?cacheBust=${Date.now()}`);
|
|
2325
|
+
const rawConfig = extractConfigExport(module);
|
|
2326
|
+
return {
|
|
2327
|
+
configPath: resolvedPath,
|
|
2328
|
+
config: normalizeGeneratorConfig(rawConfig)
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/generator/naming.ts
|
|
2333
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2334
|
+
var RESERVED_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
2335
|
+
"break",
|
|
2336
|
+
"case",
|
|
2337
|
+
"catch",
|
|
2338
|
+
"class",
|
|
2339
|
+
"const",
|
|
2340
|
+
"continue",
|
|
2341
|
+
"debugger",
|
|
2342
|
+
"default",
|
|
2343
|
+
"delete",
|
|
2344
|
+
"do",
|
|
2345
|
+
"else",
|
|
2346
|
+
"enum",
|
|
2347
|
+
"export",
|
|
2348
|
+
"extends",
|
|
2349
|
+
"false",
|
|
2350
|
+
"finally",
|
|
2351
|
+
"for",
|
|
2352
|
+
"function",
|
|
2353
|
+
"if",
|
|
2354
|
+
"import",
|
|
2355
|
+
"in",
|
|
2356
|
+
"instanceof",
|
|
2357
|
+
"new",
|
|
2358
|
+
"null",
|
|
2359
|
+
"return",
|
|
2360
|
+
"super",
|
|
2361
|
+
"switch",
|
|
2362
|
+
"this",
|
|
2363
|
+
"throw",
|
|
2364
|
+
"true",
|
|
2365
|
+
"try",
|
|
2366
|
+
"typeof",
|
|
2367
|
+
"var",
|
|
2368
|
+
"void",
|
|
2369
|
+
"while",
|
|
2370
|
+
"with",
|
|
2371
|
+
"as",
|
|
2372
|
+
"implements",
|
|
2373
|
+
"interface",
|
|
2374
|
+
"let",
|
|
2375
|
+
"package",
|
|
2376
|
+
"private",
|
|
2377
|
+
"protected",
|
|
2378
|
+
"public",
|
|
2379
|
+
"static",
|
|
2380
|
+
"yield",
|
|
2381
|
+
"any",
|
|
2382
|
+
"boolean",
|
|
2383
|
+
"constructor",
|
|
2384
|
+
"declare",
|
|
2385
|
+
"get",
|
|
2386
|
+
"module",
|
|
2387
|
+
"require",
|
|
2388
|
+
"number",
|
|
2389
|
+
"set",
|
|
2390
|
+
"string",
|
|
2391
|
+
"symbol",
|
|
2392
|
+
"type",
|
|
2393
|
+
"from",
|
|
2394
|
+
"of"
|
|
2395
|
+
]);
|
|
2396
|
+
function splitWords(input) {
|
|
2397
|
+
return input.trim().replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, "$1 $2").replace(/[^A-Za-z0-9]+/g, " ").split(" ").map((word) => word.trim()).filter((word) => word.length > 0);
|
|
2398
|
+
}
|
|
2399
|
+
function capitalize(word) {
|
|
2400
|
+
return word.length > 0 ? `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}` : word;
|
|
2401
|
+
}
|
|
2402
|
+
function ensureValidIdentifier(candidate, fallback) {
|
|
2403
|
+
const normalized = candidate.length > 0 ? candidate : fallback;
|
|
2404
|
+
const prefixed = /^[0-9]/.test(normalized) ? `_${normalized}` : normalized;
|
|
2405
|
+
if (RESERVED_IDENTIFIERS.has(prefixed)) {
|
|
2406
|
+
return `${prefixed}_value`;
|
|
2407
|
+
}
|
|
2408
|
+
return prefixed;
|
|
2409
|
+
}
|
|
2410
|
+
function applyNamingStyle(input, style) {
|
|
2411
|
+
const words = splitWords(input);
|
|
2412
|
+
if (words.length === 0) {
|
|
2413
|
+
return "";
|
|
2414
|
+
}
|
|
2415
|
+
switch (style) {
|
|
2416
|
+
case "preserve":
|
|
2417
|
+
return input;
|
|
2418
|
+
case "camel": {
|
|
2419
|
+
const [first, ...rest] = words;
|
|
2420
|
+
return `${first.toLowerCase()}${rest.map(capitalize).join("")}`;
|
|
2421
|
+
}
|
|
2422
|
+
case "pascal":
|
|
2423
|
+
return words.map(capitalize).join("");
|
|
2424
|
+
case "snake":
|
|
2425
|
+
return words.map((word) => word.toLowerCase()).join("_");
|
|
2426
|
+
case "kebab":
|
|
2427
|
+
return words.map((word) => word.toLowerCase()).join("-");
|
|
2428
|
+
default:
|
|
2429
|
+
return input;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
function toSafeIdentifier(input, style, fallback = "value") {
|
|
2433
|
+
const transformed = applyNamingStyle(input, style);
|
|
2434
|
+
const normalized = transformed.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2435
|
+
const fallbackValue = splitWords(input).join("");
|
|
2436
|
+
return ensureValidIdentifier(
|
|
2437
|
+
normalized.length > 0 ? normalized : fallbackValue,
|
|
2438
|
+
fallback
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
function escapeTypePropertyName(propertyName) {
|
|
2442
|
+
if (IDENTIFIER_PATTERN.test(propertyName) && !RESERVED_IDENTIFIERS.has(propertyName)) {
|
|
2443
|
+
return propertyName;
|
|
2444
|
+
}
|
|
2445
|
+
return `'${propertyName.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
2446
|
+
}
|
|
2447
|
+
function escapeStringLiteral(value) {
|
|
2448
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/generator/placeholders.ts
|
|
2452
|
+
function createStyleTokens(prefix, value) {
|
|
2453
|
+
return {
|
|
2454
|
+
[prefix]: value,
|
|
2455
|
+
[`${prefix}_camel`]: applyNamingStyle(value, "camel"),
|
|
2456
|
+
[`${prefix}_pascal`]: applyNamingStyle(value, "pascal"),
|
|
2457
|
+
[`${prefix}_snake`]: applyNamingStyle(value, "snake"),
|
|
2458
|
+
[`${prefix}_kebab`]: applyNamingStyle(value, "kebab")
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
function renderTemplate(template, tokenMap) {
|
|
2462
|
+
return template.replace(/\{([A-Za-z0-9_]+)\}/g, (_match, token) => {
|
|
2463
|
+
if (!(token in tokenMap)) {
|
|
2464
|
+
throw new Error(`Unknown placeholder token "${token}" in template "${template}"`);
|
|
2465
|
+
}
|
|
2466
|
+
return tokenMap[token];
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
function resolvePlaceholderMap(baseTokens, outputConfig) {
|
|
2470
|
+
const resolved = {
|
|
2471
|
+
...baseTokens
|
|
2472
|
+
};
|
|
2473
|
+
const entries = Object.entries(outputConfig.placeholderMap ?? {});
|
|
2474
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
2475
|
+
const [key, value] = entries[index];
|
|
2476
|
+
let current = value;
|
|
2477
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
2478
|
+
const next = renderTemplate(current, resolved);
|
|
2479
|
+
if (next === current) {
|
|
2480
|
+
break;
|
|
2481
|
+
}
|
|
2482
|
+
current = next;
|
|
2483
|
+
}
|
|
2484
|
+
resolved[key] = current;
|
|
2485
|
+
}
|
|
2486
|
+
return resolved;
|
|
2487
|
+
}
|
|
2488
|
+
function renderOutputPath(template, context, outputConfig) {
|
|
2489
|
+
const baseTokens = {
|
|
2490
|
+
provider: context.provider,
|
|
2491
|
+
kind: context.kind,
|
|
2492
|
+
...createStyleTokens("database", context.database),
|
|
2493
|
+
...createStyleTokens("schema", context.schema),
|
|
2494
|
+
...createStyleTokens("model", context.model)
|
|
2495
|
+
};
|
|
2496
|
+
const tokens = resolvePlaceholderMap(baseTokens, outputConfig);
|
|
2497
|
+
return renderTemplate(template, tokens);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/generator/postgres-type-mapping.ts
|
|
2501
|
+
var NUMBER_TYPES = /* @__PURE__ */ new Set([
|
|
2502
|
+
"int2",
|
|
2503
|
+
"int4",
|
|
2504
|
+
"float4",
|
|
2505
|
+
"float8",
|
|
2506
|
+
"smallint",
|
|
2507
|
+
"integer",
|
|
2508
|
+
"real",
|
|
2509
|
+
"double precision"
|
|
2510
|
+
]);
|
|
2511
|
+
var STRING_NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
2512
|
+
"int8",
|
|
2513
|
+
"bigint",
|
|
2514
|
+
"serial8",
|
|
2515
|
+
"bigserial",
|
|
2516
|
+
"numeric",
|
|
2517
|
+
"decimal",
|
|
2518
|
+
"money"
|
|
2519
|
+
]);
|
|
2520
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
2521
|
+
"char",
|
|
2522
|
+
"bpchar",
|
|
2523
|
+
"varchar",
|
|
2524
|
+
"character varying",
|
|
2525
|
+
"text",
|
|
2526
|
+
"citext",
|
|
2527
|
+
"name",
|
|
2528
|
+
"uuid",
|
|
2529
|
+
"date",
|
|
2530
|
+
"time",
|
|
2531
|
+
"timetz",
|
|
2532
|
+
"timestamp",
|
|
2533
|
+
"timestamptz",
|
|
2534
|
+
"interval",
|
|
2535
|
+
"inet",
|
|
2536
|
+
"cidr",
|
|
2537
|
+
"macaddr",
|
|
2538
|
+
"macaddr8",
|
|
2539
|
+
"point",
|
|
2540
|
+
"line",
|
|
2541
|
+
"lseg",
|
|
2542
|
+
"box",
|
|
2543
|
+
"path",
|
|
2544
|
+
"polygon",
|
|
2545
|
+
"circle",
|
|
2546
|
+
"bit",
|
|
2547
|
+
"varbit",
|
|
2548
|
+
"xml",
|
|
2549
|
+
"tsvector",
|
|
2550
|
+
"tsquery",
|
|
2551
|
+
"jsonpath"
|
|
2552
|
+
]);
|
|
2553
|
+
function normalizeTypeLabel(column) {
|
|
2554
|
+
const preferred = (column.udtName || column.dataType).toLowerCase().trim();
|
|
2555
|
+
if (column.arrayDimensions > 0 && preferred.startsWith("_")) {
|
|
2556
|
+
return preferred.slice(1);
|
|
2557
|
+
}
|
|
2558
|
+
return preferred;
|
|
2559
|
+
}
|
|
2560
|
+
function wrapArrayType(baseType, depth) {
|
|
2561
|
+
let wrapped = baseType;
|
|
2562
|
+
for (let index = 0; index < depth; index += 1) {
|
|
2563
|
+
wrapped = `Array<${wrapped}>`;
|
|
2564
|
+
}
|
|
2565
|
+
return wrapped;
|
|
2566
|
+
}
|
|
2567
|
+
function resolveScalarType(column) {
|
|
2568
|
+
const label = normalizeTypeLabel(column);
|
|
2569
|
+
if (NUMBER_TYPES.has(label)) {
|
|
2570
|
+
return "number";
|
|
2571
|
+
}
|
|
2572
|
+
if (STRING_NUMERIC_TYPES.has(label)) {
|
|
2573
|
+
return "string";
|
|
2574
|
+
}
|
|
2575
|
+
if (label === "bool" || label === "boolean") {
|
|
2576
|
+
return "boolean";
|
|
2577
|
+
}
|
|
2578
|
+
if (label === "bytea") {
|
|
2579
|
+
return "Buffer";
|
|
2580
|
+
}
|
|
2581
|
+
if (label === "json" || label === "jsonb") {
|
|
2582
|
+
return "Record<string, unknown>";
|
|
2583
|
+
}
|
|
2584
|
+
if (TEXT_TYPES.has(label)) {
|
|
2585
|
+
return "string";
|
|
2586
|
+
}
|
|
2587
|
+
if (label.endsWith("range") || label.endsWith("multirange")) {
|
|
2588
|
+
return "string";
|
|
2589
|
+
}
|
|
2590
|
+
return "unknown";
|
|
2591
|
+
}
|
|
2592
|
+
function resolveKindAwareType(column) {
|
|
2593
|
+
if (column.typeKind === "enum") {
|
|
2594
|
+
const values = column.enumValues ?? [];
|
|
2595
|
+
if (values.length === 0) {
|
|
2596
|
+
return "string";
|
|
2597
|
+
}
|
|
2598
|
+
return values.map((value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`).join(" | ");
|
|
2599
|
+
}
|
|
2600
|
+
if (column.typeKind === "composite") {
|
|
2601
|
+
return "Record<string, unknown>";
|
|
2602
|
+
}
|
|
2603
|
+
if (column.typeKind === "domain" || column.typeKind === "range" || column.typeKind === "multirange") {
|
|
2604
|
+
return "string";
|
|
2605
|
+
}
|
|
2606
|
+
return resolveScalarType(column);
|
|
2607
|
+
}
|
|
2608
|
+
function resolvePostgresColumnType(column) {
|
|
2609
|
+
const baseType = resolveKindAwareType(column);
|
|
2610
|
+
if (!column.arrayDimensions || column.arrayDimensions <= 0) {
|
|
2611
|
+
return baseType;
|
|
2612
|
+
}
|
|
2613
|
+
return wrapArrayType(baseType, column.arrayDimensions);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// src/generator/renderer.ts
|
|
2617
|
+
function normalizePath(pathValue) {
|
|
2618
|
+
return pathValue.replace(/\\/g, "/");
|
|
2619
|
+
}
|
|
2620
|
+
function withoutTypeScriptExtension(pathValue) {
|
|
2621
|
+
return pathValue.replace(/\.tsx?$/i, "");
|
|
2622
|
+
}
|
|
2623
|
+
function toModuleImportPath(fromFile, targetFile) {
|
|
2624
|
+
const relativePath = withoutTypeScriptExtension(
|
|
2625
|
+
normalizePath(posix.relative(posix.dirname(fromFile), targetFile))
|
|
2626
|
+
);
|
|
2627
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
2628
|
+
}
|
|
2629
|
+
function renderObjectKey(key) {
|
|
2630
|
+
const escaped = escapeTypePropertyName(key);
|
|
2631
|
+
return escaped.startsWith("'") ? escaped : escaped;
|
|
2632
|
+
}
|
|
2633
|
+
function renderRelation(relation) {
|
|
2634
|
+
const through = relation.through ? `,
|
|
2635
|
+
through: {
|
|
2636
|
+
schema: ${escapeStringLiteral(relation.through.schema)},
|
|
2637
|
+
model: ${escapeStringLiteral(relation.through.model)},
|
|
2638
|
+
sourceColumns: [${relation.through.sourceColumns.map((value) => escapeStringLiteral(value)).join(", ")}],
|
|
2639
|
+
targetColumns: [${relation.through.targetColumns.map((value) => escapeStringLiteral(value)).join(", ")}],
|
|
2640
|
+
}` : "";
|
|
2641
|
+
return `{
|
|
2642
|
+
kind: ${escapeStringLiteral(relation.kind)},
|
|
2643
|
+
sourceColumns: [${relation.sourceColumns.map((value) => escapeStringLiteral(value)).join(", ")}],
|
|
2644
|
+
targetSchema: ${escapeStringLiteral(relation.targetSchema)},
|
|
2645
|
+
targetModel: ${escapeStringLiteral(relation.targetModel)},
|
|
2646
|
+
targetColumns: [${relation.targetColumns.map((value) => escapeStringLiteral(value)).join(", ")}]${through}
|
|
2647
|
+
}`;
|
|
2648
|
+
}
|
|
2649
|
+
function renderModelArtifact(snapshot, descriptor, config) {
|
|
2650
|
+
const columnLines = Object.values(descriptor.table.columns).map((column) => {
|
|
2651
|
+
const propertyName = escapeTypePropertyName(column.name);
|
|
2652
|
+
const baseType = resolvePostgresColumnType(column);
|
|
2653
|
+
const isOptional = column.isNullable;
|
|
2654
|
+
const typeWithNullability = column.isNullable ? `${baseType} | null` : baseType;
|
|
2655
|
+
return ` ${propertyName}${isOptional ? "?" : ""}: ${typeWithNullability}`;
|
|
2656
|
+
}).join("\n");
|
|
2657
|
+
const nullableLines = Object.values(descriptor.table.columns).map((column) => ` ${renderObjectKey(column.name)}: ${column.isNullable ? "true" : "false"}`).join(",\n");
|
|
2658
|
+
const relationEntries = Object.entries(descriptor.table.relations);
|
|
2659
|
+
const relationBlock = config.features.emitRelations && relationEntries.length > 0 ? `,
|
|
2660
|
+
relations: {
|
|
2661
|
+
${relationEntries.map(([relationKey2, relationValue]) => ` ${renderObjectKey(relationKey2)}: ${renderRelation(relationValue)}`).join(",\n")}
|
|
2662
|
+
}` : "";
|
|
2663
|
+
const content = `import { defineModel } from '@xylex-group/athena'
|
|
2664
|
+
|
|
2665
|
+
export interface ${descriptor.rowTypeName} {
|
|
2666
|
+
${columnLines}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
export type ${descriptor.insertTypeName} = Partial<${descriptor.rowTypeName}>
|
|
2670
|
+
export type ${descriptor.updateTypeName} = Partial<${descriptor.insertTypeName}>
|
|
2671
|
+
|
|
2672
|
+
export const ${descriptor.modelConstName} = defineModel<${descriptor.rowTypeName}, ${descriptor.insertTypeName}, ${descriptor.updateTypeName}>({
|
|
2673
|
+
meta: {
|
|
2674
|
+
database: ${escapeStringLiteral(snapshot.database)},
|
|
2675
|
+
schema: ${escapeStringLiteral(descriptor.schemaName)},
|
|
2676
|
+
model: ${escapeStringLiteral(descriptor.tableName)},
|
|
2677
|
+
tableName: ${escapeStringLiteral(`${descriptor.schemaName}.${descriptor.tableName}`)},
|
|
2678
|
+
primaryKey: [${descriptor.table.primaryKey.map((value) => escapeStringLiteral(value)).join(", ")}],
|
|
2679
|
+
nullable: {
|
|
2680
|
+
${nullableLines}
|
|
2681
|
+
}${relationBlock}
|
|
2682
|
+
}
|
|
2683
|
+
})
|
|
2684
|
+
`;
|
|
2685
|
+
return {
|
|
2686
|
+
kind: "model",
|
|
2687
|
+
path: descriptor.filePath,
|
|
2688
|
+
content
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
function renderSchemaArtifact(descriptor) {
|
|
2692
|
+
const importLines = descriptor.models.map((modelDescriptor) => {
|
|
2693
|
+
const importPath = toModuleImportPath(descriptor.filePath, modelDescriptor.filePath);
|
|
2694
|
+
return `import { ${modelDescriptor.modelConstName} } from '${importPath}'`;
|
|
2695
|
+
}).join("\n");
|
|
2696
|
+
const modelEntries = descriptor.models.map((modelDescriptor) => ` ${renderObjectKey(modelDescriptor.tableName)}: ${modelDescriptor.modelConstName}`).join(",\n");
|
|
2697
|
+
const content = `import { defineSchema } from '@xylex-group/athena'
|
|
2698
|
+
${importLines ? `
|
|
2699
|
+
${importLines}
|
|
2700
|
+
` : "\n"}
|
|
2701
|
+
export const ${descriptor.schemaConstName} = defineSchema({
|
|
2702
|
+
${modelEntries}
|
|
2703
|
+
})
|
|
2704
|
+
`;
|
|
2705
|
+
return {
|
|
2706
|
+
kind: "schema",
|
|
2707
|
+
path: descriptor.filePath,
|
|
2708
|
+
content
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function renderDatabaseArtifact(descriptor) {
|
|
2712
|
+
const importLines = descriptor.schemas.map((schemaDescriptor) => {
|
|
2713
|
+
const importPath = toModuleImportPath(descriptor.filePath, schemaDescriptor.filePath);
|
|
2714
|
+
return `import { ${schemaDescriptor.schemaConstName} } from '${importPath}'`;
|
|
2715
|
+
}).join("\n");
|
|
2716
|
+
const schemaEntries = descriptor.schemas.map((schemaDescriptor) => ` ${renderObjectKey(schemaDescriptor.schemaName)}: ${schemaDescriptor.schemaConstName}`).join(",\n");
|
|
2717
|
+
const content = `import { defineDatabase } from '@xylex-group/athena'
|
|
2718
|
+
${importLines ? `
|
|
2719
|
+
${importLines}
|
|
2720
|
+
` : "\n"}
|
|
2721
|
+
export const ${descriptor.databaseConstName} = defineDatabase({
|
|
2722
|
+
${schemaEntries}
|
|
2723
|
+
})
|
|
2724
|
+
`;
|
|
2725
|
+
return {
|
|
2726
|
+
kind: "database",
|
|
2727
|
+
path: descriptor.filePath,
|
|
2728
|
+
content
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function renderRegistryArtifact(registryPath, databasePath, databaseConstName, registryConstName, databaseName) {
|
|
2732
|
+
const databaseImportPath = toModuleImportPath(registryPath, databasePath);
|
|
2733
|
+
const content = `import { defineRegistry } from '@xylex-group/athena'
|
|
2734
|
+
import { ${databaseConstName} } from '${databaseImportPath}'
|
|
2735
|
+
|
|
2736
|
+
export const ${registryConstName} = defineRegistry({
|
|
2737
|
+
${renderObjectKey(databaseName)}: ${databaseConstName}
|
|
2738
|
+
})
|
|
2739
|
+
`;
|
|
2740
|
+
return {
|
|
2741
|
+
kind: "registry",
|
|
2742
|
+
path: registryPath,
|
|
2743
|
+
content
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
function assertNoDuplicatePaths(files) {
|
|
2747
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2748
|
+
for (const file of files) {
|
|
2749
|
+
if (seen.has(file.path)) {
|
|
2750
|
+
throw new Error(`Generator output collision detected for path: ${file.path}`);
|
|
2751
|
+
}
|
|
2752
|
+
seen.add(file.path);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
var ArtifactComposer = class {
|
|
2756
|
+
constructor(snapshot, config) {
|
|
2757
|
+
this.snapshot = snapshot;
|
|
2758
|
+
this.config = config;
|
|
2759
|
+
}
|
|
2760
|
+
compose() {
|
|
2761
|
+
const providerName = this.snapshot.backend;
|
|
2762
|
+
const databaseName = this.snapshot.database;
|
|
2763
|
+
const modelDescriptors = [];
|
|
2764
|
+
for (const schemaName of Object.keys(this.snapshot.schemas).sort()) {
|
|
2765
|
+
const schema = this.snapshot.schemas[schemaName];
|
|
2766
|
+
for (const tableName of Object.keys(schema.tables).sort()) {
|
|
2767
|
+
const table = schema.tables[tableName];
|
|
2768
|
+
const rowTypeName = `${toSafeIdentifier(`${schemaName} ${tableName}`, this.config.naming.modelType, "Model")}Row`;
|
|
2769
|
+
const insertTypeName = `${toSafeIdentifier(`${schemaName} ${tableName}`, this.config.naming.modelType, "Model")}Insert`;
|
|
2770
|
+
const updateTypeName = `${toSafeIdentifier(`${schemaName} ${tableName}`, this.config.naming.modelType, "Model")}Update`;
|
|
2771
|
+
const modelConstName = `${toSafeIdentifier(`${schemaName} ${tableName} model`, this.config.naming.modelConst, "model")}`;
|
|
2772
|
+
const modelPath = normalizePath(
|
|
2773
|
+
renderOutputPath(this.config.output.targets.model, {
|
|
2774
|
+
provider: providerName,
|
|
2775
|
+
kind: "model",
|
|
2776
|
+
database: databaseName,
|
|
2777
|
+
schema: schemaName,
|
|
2778
|
+
model: tableName
|
|
2779
|
+
}, this.config.output)
|
|
2780
|
+
);
|
|
2781
|
+
modelDescriptors.push({
|
|
2782
|
+
schemaName,
|
|
2783
|
+
tableName,
|
|
2784
|
+
filePath: modelPath,
|
|
2785
|
+
rowTypeName,
|
|
2786
|
+
insertTypeName,
|
|
2787
|
+
updateTypeName,
|
|
2788
|
+
modelConstName,
|
|
2789
|
+
table
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const schemaDescriptors = Object.keys(this.snapshot.schemas).sort().map((schemaName) => {
|
|
2794
|
+
const schemaPath = normalizePath(
|
|
2795
|
+
renderOutputPath(this.config.output.targets.schema, {
|
|
2796
|
+
provider: providerName,
|
|
2797
|
+
kind: "schema",
|
|
2798
|
+
database: databaseName,
|
|
2799
|
+
schema: schemaName,
|
|
2800
|
+
model: "index"
|
|
2801
|
+
}, this.config.output)
|
|
2802
|
+
);
|
|
2803
|
+
return {
|
|
2804
|
+
schemaName,
|
|
2805
|
+
filePath: schemaPath,
|
|
2806
|
+
schemaConstName: toSafeIdentifier(
|
|
2807
|
+
`${schemaName} schema`,
|
|
2808
|
+
this.config.naming.schemaConst,
|
|
2809
|
+
"schema"
|
|
2810
|
+
),
|
|
2811
|
+
models: modelDescriptors.filter((model) => model.schemaName === schemaName)
|
|
2812
|
+
};
|
|
2813
|
+
});
|
|
2814
|
+
const databasePath = normalizePath(
|
|
2815
|
+
renderOutputPath(this.config.output.targets.database, {
|
|
2816
|
+
provider: providerName,
|
|
2817
|
+
kind: "database",
|
|
2818
|
+
database: databaseName,
|
|
2819
|
+
schema: "index",
|
|
2820
|
+
model: "index"
|
|
2821
|
+
}, this.config.output)
|
|
2822
|
+
);
|
|
2823
|
+
const databaseDescriptor = {
|
|
2824
|
+
filePath: databasePath,
|
|
2825
|
+
databaseConstName: toSafeIdentifier(
|
|
2826
|
+
`${databaseName} database`,
|
|
2827
|
+
this.config.naming.databaseConst,
|
|
2828
|
+
"database"
|
|
2829
|
+
),
|
|
2830
|
+
schemas: schemaDescriptors
|
|
2831
|
+
};
|
|
2832
|
+
const files = [];
|
|
2833
|
+
for (const modelDescriptor of modelDescriptors) {
|
|
2834
|
+
files.push(renderModelArtifact(this.snapshot, modelDescriptor, this.config));
|
|
2835
|
+
}
|
|
2836
|
+
for (const schemaDescriptor of schemaDescriptors) {
|
|
2837
|
+
files.push(renderSchemaArtifact(schemaDescriptor));
|
|
2838
|
+
}
|
|
2839
|
+
files.push(renderDatabaseArtifact(databaseDescriptor));
|
|
2840
|
+
if (this.config.features.emitRegistry) {
|
|
2841
|
+
const registryPath = normalizePath(
|
|
2842
|
+
renderOutputPath(this.config.output.targets.registry, {
|
|
2843
|
+
provider: providerName,
|
|
2844
|
+
kind: "registry",
|
|
2845
|
+
database: databaseName,
|
|
2846
|
+
schema: "index",
|
|
2847
|
+
model: "index"
|
|
2848
|
+
}, this.config.output)
|
|
2849
|
+
);
|
|
2850
|
+
files.push(
|
|
2851
|
+
renderRegistryArtifact(
|
|
2852
|
+
registryPath,
|
|
2853
|
+
databaseDescriptor.filePath,
|
|
2854
|
+
databaseDescriptor.databaseConstName,
|
|
2855
|
+
toSafeIdentifier("registry", this.config.naming.registryConst, "registry"),
|
|
2856
|
+
databaseName
|
|
2857
|
+
)
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
assertNoDuplicatePaths(files);
|
|
2861
|
+
return {
|
|
2862
|
+
snapshot: this.snapshot,
|
|
2863
|
+
files
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
function generateArtifactsFromSnapshot(snapshot, config) {
|
|
2868
|
+
const normalizedConfig = "naming" in config && "features" in config && "experimental" in config ? config : normalizeGeneratorConfig(config);
|
|
2869
|
+
return new ArtifactComposer(snapshot, normalizedConfig).compose();
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// src/generator/providers.ts
|
|
2873
|
+
var AthenaGatewayCatalogClient = class {
|
|
2874
|
+
constructor(client) {
|
|
2875
|
+
this.client = client;
|
|
2876
|
+
}
|
|
2877
|
+
async queryRows(query) {
|
|
2878
|
+
const result = await this.client.query(query);
|
|
2879
|
+
if (result.error || result.status < 200 || result.status >= 300) {
|
|
2880
|
+
throw new Error(result.error ?? `Gateway query failed with status ${result.status}`);
|
|
2881
|
+
}
|
|
2882
|
+
return result.data ?? [];
|
|
2883
|
+
}
|
|
2884
|
+
async queryColumns(query) {
|
|
2885
|
+
return this.queryRows(query);
|
|
2886
|
+
}
|
|
2887
|
+
async queryEnums(query) {
|
|
2888
|
+
const rows = await this.queryRows(query);
|
|
2889
|
+
const enumMap = /* @__PURE__ */ new Map();
|
|
2890
|
+
for (const row of rows) {
|
|
2891
|
+
const existing = enumMap.get(row.type_oid) ?? [];
|
|
2892
|
+
existing.push(row.enum_label);
|
|
2893
|
+
enumMap.set(row.type_oid, existing);
|
|
2894
|
+
}
|
|
2895
|
+
return enumMap;
|
|
2896
|
+
}
|
|
2897
|
+
async queryPrimaryKeys(query) {
|
|
2898
|
+
return this.queryRows(query);
|
|
2899
|
+
}
|
|
2900
|
+
async queryForeignKeys(query) {
|
|
2901
|
+
return this.queryRows(query);
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
var AthenaGatewayPostgresIntrospectionProvider = class {
|
|
2905
|
+
constructor(config) {
|
|
2906
|
+
this.config = config;
|
|
2907
|
+
this.client = createClient(this.config.gatewayUrl, this.config.apiKey, {
|
|
2908
|
+
backend: {
|
|
2909
|
+
type: this.config.backend ?? "postgresql"
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
backend = "postgresql";
|
|
2914
|
+
client;
|
|
2915
|
+
async inspect(options) {
|
|
2916
|
+
const schemas = options?.schemas && options.schemas.length > 0 ? options.schemas : this.config.schemas && this.config.schemas.length > 0 ? this.config.schemas : ["public"];
|
|
2917
|
+
const catalogClient = new AthenaGatewayCatalogClient(this.client);
|
|
2918
|
+
const queries = buildGatewayCatalogQueries(schemas);
|
|
2919
|
+
const [columnRows, enumMap, primaryKeyRows, foreignKeyRows] = await Promise.all([
|
|
2920
|
+
catalogClient.queryColumns(queries.columns),
|
|
2921
|
+
catalogClient.queryEnums(queries.enums),
|
|
2922
|
+
catalogClient.queryPrimaryKeys(queries.primaryKeys),
|
|
2923
|
+
catalogClient.queryForeignKeys(queries.foreignKeys)
|
|
2924
|
+
]);
|
|
2925
|
+
const assembler = new PostgresCatalogSnapshotAssembler();
|
|
2926
|
+
assembler.addColumnRows(columnRows, enumMap);
|
|
2927
|
+
assembler.addPrimaryKeyRows(primaryKeyRows);
|
|
2928
|
+
assembler.addForeignKeyRows(foreignKeyRows);
|
|
2929
|
+
assembler.addManyToManyRows(foreignKeyRows);
|
|
2930
|
+
return {
|
|
2931
|
+
backend: "postgresql",
|
|
2932
|
+
database: this.config.database,
|
|
2933
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2934
|
+
schemas: assembler.toSchemas()
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
};
|
|
2938
|
+
var ScyllaIntrospectionProvider = class {
|
|
2939
|
+
constructor(config) {
|
|
2940
|
+
this.config = config;
|
|
2941
|
+
}
|
|
2942
|
+
backend = "scylladb";
|
|
2943
|
+
async inspect() {
|
|
2944
|
+
throw new Error(
|
|
2945
|
+
`Scylla introspection provider is not implemented yet for keyspace ${this.config.keyspace}.`
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
function createPostgresProvider(config) {
|
|
2950
|
+
return createPostgresIntrospectionProvider({
|
|
2951
|
+
connectionString: config.connectionString,
|
|
2952
|
+
database: config.database
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
function resolveGeneratorProvider(providerConfig, experimentalFlags) {
|
|
2956
|
+
if (providerConfig.kind === "postgres" && providerConfig.mode === "direct") {
|
|
2957
|
+
return createPostgresProvider(providerConfig);
|
|
2958
|
+
}
|
|
2959
|
+
if (providerConfig.kind === "postgres" && providerConfig.mode === "gateway") {
|
|
2960
|
+
return new AthenaGatewayPostgresIntrospectionProvider(providerConfig);
|
|
2961
|
+
}
|
|
2962
|
+
if (providerConfig.kind === "scylla") {
|
|
2963
|
+
if (!experimentalFlags.scyllaProviderContracts) {
|
|
2964
|
+
throw new Error(
|
|
2965
|
+
"Scylla provider contracts are disabled. Set experimental.scyllaProviderContracts=true to enable placeholders."
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
return new ScyllaIntrospectionProvider(providerConfig);
|
|
2969
|
+
}
|
|
2970
|
+
throw new Error(`Unsupported generator provider kind: ${providerConfig.kind ?? "unknown"}`);
|
|
2971
|
+
}
|
|
2972
|
+
function extractProviderSchemas(providerConfig) {
|
|
2973
|
+
if (!("schemas" in providerConfig) || !providerConfig.schemas || providerConfig.schemas.length === 0) {
|
|
2974
|
+
return void 0;
|
|
2975
|
+
}
|
|
2976
|
+
return providerConfig.schemas;
|
|
2977
|
+
}
|
|
2978
|
+
async function writeArtifacts(files, cwd) {
|
|
2979
|
+
const writtenFiles = [];
|
|
2980
|
+
for (const file of files) {
|
|
2981
|
+
const absolutePath = resolve(cwd, file.path);
|
|
2982
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
2983
|
+
await writeFile(absolutePath, file.content, "utf8");
|
|
2984
|
+
writtenFiles.push(file.path);
|
|
2985
|
+
}
|
|
2986
|
+
return writtenFiles;
|
|
2987
|
+
}
|
|
2988
|
+
async function runSchemaGenerator(options = {}) {
|
|
2989
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2990
|
+
const configOptions = {
|
|
2991
|
+
cwd,
|
|
2992
|
+
configPath: options.configPath
|
|
2993
|
+
};
|
|
2994
|
+
const { configPath, config } = await loadGeneratorConfig(configOptions);
|
|
2995
|
+
const provider = options.provider ?? resolveGeneratorProvider(config.provider, config.experimental);
|
|
2996
|
+
const snapshot = await provider.inspect({
|
|
2997
|
+
schemas: extractProviderSchemas(config.provider)
|
|
2998
|
+
});
|
|
2999
|
+
const generated = generateArtifactsFromSnapshot(snapshot, config);
|
|
3000
|
+
const writtenFiles = options.dryRun ? [] : await writeArtifacts(generated.files, cwd);
|
|
3001
|
+
return {
|
|
3002
|
+
...generated,
|
|
3003
|
+
configPath,
|
|
3004
|
+
writtenFiles
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
export { AthenaClient, AthenaError, AthenaErrorCategory, AthenaErrorCode, AthenaErrorKind, AthenaGatewayError, Backend, assertInt, coerceInt, createClient, createPostgresIntrospectionProvider, createTypedClient, defineDatabase, defineGeneratorConfig, defineModel, defineRegistry, defineSchema, findGeneratorConfigPath, generateArtifactsFromSnapshot, identifier, isAthenaGatewayError, isOk, loadGeneratorConfig, normalizeAthenaError, normalizeGeneratorConfig, requireAffected, requireSuccess, resolveGeneratorProvider, resolvePostgresColumnType, runSchemaGenerator, unwrap, unwrapOne, unwrapRows, withRetry };
|
|
1489
3009
|
//# sourceMappingURL=index.js.map
|
|
1490
3010
|
//# sourceMappingURL=index.js.map
|