@xylex-group/athena 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -6
- package/bin/athena-js.js +74 -6
- package/dist/cli/index.cjs +233 -40
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +21 -2
- package/dist/cli/index.d.ts +21 -2
- package/dist/cli/index.js +232 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/errors-BJGgjHcI.d.cts +31 -0
- package/dist/errors-Bcf5Sftv.d.ts +31 -0
- package/dist/index.cjs +638 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -335
- package/dist/index.d.ts +266 -335
- package/dist/index.js +635 -27
- package/dist/index.js.map +1 -1
- package/dist/pipeline-BsKuBqmE.d.cts +343 -0
- package/dist/pipeline-xQSjGbqF.d.ts +343 -0
- package/dist/react.d.cts +3 -2
- package/dist/react.d.ts +3 -2
- package/dist/{errors-C4GJaFI_.d.cts → types-wPA1Z4vQ.d.cts} +1 -29
- package/dist/{errors-C4GJaFI_.d.ts → types-wPA1Z4vQ.d.ts} +1 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# athena-js
|
|
2
2
|
|
|
3
|
-
current version: `1.
|
|
3
|
+
current version: `1.7.0`
|
|
4
4
|
`@xylex-group/athena` is a database driver and API gateway SDK that lets you interact with SQL backends over HTTP through a fluent builder API. It ships a typed query builder for Node.js / server environments plus Athena-native React hooks for client-side use.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -43,6 +43,51 @@ if (error) {
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Auth client (Athena Auth server)
|
|
47
|
+
|
|
48
|
+
If your auth backend is now Athena Auth, you can keep core login/session flows in this SDK:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createAuthClient } from "@xylex-group/athena";
|
|
52
|
+
|
|
53
|
+
const auth = createAuthClient({
|
|
54
|
+
baseUrl: "http://localhost:3001/api/auth",
|
|
55
|
+
// optional: bearer token if you are not using cookie-based sessions
|
|
56
|
+
bearerToken: process.env.AUTH_BEARER_TOKEN,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const login = await auth.signIn.email({
|
|
60
|
+
email: "demo@example.com",
|
|
61
|
+
password: "super-secret",
|
|
62
|
+
rememberMe: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const session = await auth.getSession();
|
|
66
|
+
const sessions = await auth.listSessions();
|
|
67
|
+
|
|
68
|
+
// clear one session
|
|
69
|
+
await auth.revokeSession({ token: "session_token_here" });
|
|
70
|
+
// or clear all sessions
|
|
71
|
+
await auth.revokeSessions();
|
|
72
|
+
|
|
73
|
+
await auth.signOut();
|
|
74
|
+
|
|
75
|
+
// additional core flows
|
|
76
|
+
await auth.forgetPassword({ email: "demo@example.com", redirectTo: "https://app/reset-password" });
|
|
77
|
+
await auth.resetPassword({ newPassword: "new-secret", token: "reset_token" });
|
|
78
|
+
await auth.verifyEmail({ token: "verify_token", callbackURL: "https://app/verified" });
|
|
79
|
+
await auth.changePassword({ currentPassword: "old-secret", newPassword: "new-secret" });
|
|
80
|
+
await auth.updateUser({ name: "Demo User" });
|
|
81
|
+
|
|
82
|
+
// escape hatch for any endpoint on your Athena Auth instance
|
|
83
|
+
await auth.request({
|
|
84
|
+
endpoint: "/ok",
|
|
85
|
+
query: { ping: "pong" },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Auth responses follow the same envelope style: `{ ok, status, data, error, errorDetails, raw }`.
|
|
90
|
+
|
|
46
91
|
### Typed schema registry (model-first)
|
|
47
92
|
|
|
48
93
|
You can keep `createClient(...).from<T>(...)` as-is, or opt into a typed registry:
|
|
@@ -83,22 +128,46 @@ await typed
|
|
|
83
128
|
|
|
84
129
|
For full details, see [`docs/typed-schema-registry.md`](./docs/typed-schema-registry.md).
|
|
85
130
|
|
|
86
|
-
### Typed schema generator
|
|
131
|
+
### Typed schema generator
|
|
132
|
+
|
|
133
|
+
Schema generation is additive. Existing `createClient(...).from<T>(...)` usage remains valid while teams migrate to generated registry files.
|
|
87
134
|
|
|
88
|
-
|
|
135
|
+
CLI:
|
|
89
136
|
|
|
90
137
|
```bash
|
|
91
138
|
athena-js generate
|
|
92
139
|
athena-js generate --dry-run
|
|
93
140
|
athena-js generate --config ./athena.config.ts
|
|
141
|
+
athena-js generate --help
|
|
142
|
+
athena-js help generate
|
|
94
143
|
```
|
|
95
144
|
|
|
96
|
-
Generator
|
|
97
|
-
|
|
145
|
+
Generator supports:
|
|
146
|
+
|
|
147
|
+
- PostgreSQL direct introspection (`provider.mode = "direct"`, `provider.connectionString` from your `PG_URL`/`DATABASE_URL`)
|
|
148
|
+
- PostgreSQL gateway-only introspection (`provider.mode = "gateway"` via Athena `POST /gateway/query`)
|
|
149
|
+
- Multiple schema syncs such as `public` plus `athena`, with schema-safe default output paths
|
|
150
|
+
- Placeholder-driven output paths
|
|
151
|
+
- Feature flags (`features.emitRegistry`, `features.emitRelations`)
|
|
98
152
|
|
|
99
|
-
For full generator configuration, see [`docs/generator-config.md`](./docs/generator-config.md).
|
|
153
|
+
For full generator configuration and troubleshooting, see [`docs/generator-config.md`](./docs/generator-config.md).
|
|
154
|
+
For full CLI commands, help behavior, and troubleshooting, see [`docs/cli-command-reference.md`](./docs/cli-command-reference.md).
|
|
155
|
+
For CI/CD pipelines and generated-file branch policy, see [`docs/generator-cicd.md`](./docs/generator-cicd.md).
|
|
100
156
|
For prompt-ready documentation handoff text, see [`docs/generator-codex-handoff-prompt-pack.md`](./docs/generator-codex-handoff-prompt-pack.md).
|
|
101
157
|
|
|
158
|
+
### Athena JS and Athena RS
|
|
159
|
+
|
|
160
|
+
`athena-js` is designed to be standalone for TypeScript/Node and React-native workflows:
|
|
161
|
+
|
|
162
|
+
- query builder + hooks
|
|
163
|
+
- typed registry and generator pipeline
|
|
164
|
+
- CLI-driven codegen in JS/TS projects
|
|
165
|
+
|
|
166
|
+
`athena-rs` remains the faster fit for Rust service execution paths. Teams can run both in parallel:
|
|
167
|
+
|
|
168
|
+
- `athena-rs` for Rust backend throughput
|
|
169
|
+
- `athena-js` for app/tooling layers that need TypeScript contracts and frontend-facing ergonomics
|
|
170
|
+
|
|
102
171
|
Every query resolves to `{ data, error, errorDetails?, status, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
|
|
103
172
|
|
|
104
173
|
For richer handling, inspect `errorDetails` (`code`, `status`, `endpoint`, `method`, `requestId`, etc.) or use `AthenaGatewayError` / `isAthenaGatewayError` from the package exports.
|
|
@@ -666,6 +735,8 @@ CI and publish workflows run `typecheck` before build/publish.
|
|
|
666
735
|
|
|
667
736
|
- [Documentation index](docs/index.md) — complete documentation map
|
|
668
737
|
- [Getting started](docs/getting-started.md) — step-by-step walkthrough
|
|
738
|
+
- [CLI command reference](docs/cli-command-reference.md) — all `athena-js` commands, help flows, and troubleshooting
|
|
669
739
|
- [Typed schema registry](docs/typed-schema-registry.md) — typed contracts and migration path
|
|
670
740
|
- [Generator config](docs/generator-config.md) — generator provider and output pipeline
|
|
741
|
+
- [Generator CI/CD](docs/generator-cicd.md) — pipeline patterns, secret mapping, retries, and branch policy
|
|
671
742
|
- [API reference](docs/api-reference.md) — complete method and type reference
|
package/bin/athena-js.js
CHANGED
|
@@ -1,8 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const binPath = fileURLToPath(import.meta.url)
|
|
8
|
+
const packageRoot = path.resolve(path.dirname(binPath), '..')
|
|
9
|
+
const cliEntrypointPath = path.resolve(packageRoot, 'dist', 'cli', 'index.js')
|
|
10
|
+
const packageJsonPath = path.resolve(packageRoot, 'package.json')
|
|
11
|
+
|
|
12
|
+
function getInstalledVersion() {
|
|
13
|
+
try {
|
|
14
|
+
const packageJsonRaw = readFileSync(packageJsonPath, 'utf8')
|
|
15
|
+
const packageJson = JSON.parse(packageJsonRaw)
|
|
16
|
+
return typeof packageJson.version === 'string' ? packageJson.version : 'unknown'
|
|
17
|
+
} catch {
|
|
18
|
+
return 'unknown'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printMissingEntrypointError() {
|
|
23
|
+
const installedVersion = getInstalledVersion()
|
|
24
|
+
console.error(
|
|
25
|
+
[
|
|
26
|
+
'Failed to start athena-js CLI: package install is missing the generated CLI entrypoint.',
|
|
27
|
+
`Expected file: ${cliEntrypointPath}`,
|
|
28
|
+
`Installed package version: ${installedVersion}`,
|
|
29
|
+
'',
|
|
30
|
+
'Fix by reinstalling the latest package:',
|
|
31
|
+
' pnpm add -g @xylex-group/athena@latest',
|
|
32
|
+
].join('\n'),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatRuntimeError(error) {
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
if (process.env.ATHENA_JS_DEBUG === '1') {
|
|
39
|
+
return error.stack ?? error.message;
|
|
40
|
+
}
|
|
41
|
+
return error.message;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof error === 'string') {
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return 'Unknown error.';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
if (!existsSync(cliEntrypointPath)) {
|
|
53
|
+
printMissingEntrypointError();
|
|
54
|
+
process.exit(1);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const cliEntrypointUrl = pathToFileURL(cliEntrypointPath).href;
|
|
60
|
+
const cliModule = await import(cliEntrypointUrl);
|
|
61
|
+
if (typeof cliModule.runCLI !== 'function') {
|
|
62
|
+
throw new Error('CLI module does not export runCLI.');
|
|
63
|
+
}
|
|
64
|
+
await cliModule.runCLI(process.argv.slice(2));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const errorDetail = formatRuntimeError(err);
|
|
67
|
+
if (errorDetail.includes('\n')) {
|
|
68
|
+
console.error(`Failed to start athena-js CLI:\n${errorDetail}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`Failed to start athena-js CLI: ${errorDetail}`);
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
void main()
|
package/dist/cli/index.cjs
CHANGED
|
@@ -291,6 +291,37 @@ function resolvePostgresColumnType(column) {
|
|
|
291
291
|
}
|
|
292
292
|
return wrapArrayType(baseType, column.arrayDimensions);
|
|
293
293
|
}
|
|
294
|
+
|
|
295
|
+
// src/generator/schema-selection.ts
|
|
296
|
+
var DEFAULT_POSTGRES_SCHEMAS = ["public"];
|
|
297
|
+
function collectSchemaNames(input) {
|
|
298
|
+
if (!input) {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const values = typeof input === "string" ? [input] : input;
|
|
302
|
+
return values.flatMap((value) => String(value).split(","));
|
|
303
|
+
}
|
|
304
|
+
function normalizeSchemaSelection(input) {
|
|
305
|
+
const schemas = [];
|
|
306
|
+
const seen = /* @__PURE__ */ new Set();
|
|
307
|
+
for (const value of collectSchemaNames(input)) {
|
|
308
|
+
const schema = value.trim();
|
|
309
|
+
if (!schema || seen.has(schema)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
seen.add(schema);
|
|
313
|
+
schemas.push(schema);
|
|
314
|
+
}
|
|
315
|
+
return schemas.length > 0 ? schemas : [...DEFAULT_POSTGRES_SCHEMAS];
|
|
316
|
+
}
|
|
317
|
+
function resolveProviderSchemas(providerConfig) {
|
|
318
|
+
if (providerConfig.kind === "postgres") {
|
|
319
|
+
return normalizeSchemaSelection(providerConfig.schemas);
|
|
320
|
+
}
|
|
321
|
+
return [...DEFAULT_POSTGRES_SCHEMAS];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/generator/config.ts
|
|
294
325
|
var DEFAULT_CONFIG_CANDIDATES = [
|
|
295
326
|
"athena.config.ts",
|
|
296
327
|
"athena.config.js",
|
|
@@ -300,10 +331,10 @@ var DEFAULT_CONFIG_CANDIDATES = [
|
|
|
300
331
|
".athena.config.js"
|
|
301
332
|
];
|
|
302
333
|
var DEFAULT_TARGETS = {
|
|
303
|
-
model: "
|
|
304
|
-
schema: "
|
|
305
|
-
database: "
|
|
306
|
-
registry: "
|
|
334
|
+
model: "athena/models/{schema_kebab}/{model_kebab}.ts",
|
|
335
|
+
schema: "athena/schemas/{schema_kebab}.ts",
|
|
336
|
+
database: "athena/relations.ts",
|
|
337
|
+
registry: "athena/config.ts"
|
|
307
338
|
};
|
|
308
339
|
var DEFAULT_NAMING = {
|
|
309
340
|
modelType: "pascal",
|
|
@@ -331,6 +362,15 @@ function normalizeOutputConfig(output) {
|
|
|
331
362
|
}
|
|
332
363
|
};
|
|
333
364
|
}
|
|
365
|
+
function normalizeProviderConfig(provider) {
|
|
366
|
+
if (provider.kind === "postgres") {
|
|
367
|
+
return {
|
|
368
|
+
...provider,
|
|
369
|
+
schemas: normalizeSchemaSelection(provider.schemas)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return provider;
|
|
373
|
+
}
|
|
334
374
|
function isAthenaGeneratorConfig(value) {
|
|
335
375
|
if (!value || typeof value !== "object") {
|
|
336
376
|
return false;
|
|
@@ -340,7 +380,7 @@ function isAthenaGeneratorConfig(value) {
|
|
|
340
380
|
}
|
|
341
381
|
function normalizeGeneratorConfig(input) {
|
|
342
382
|
return {
|
|
343
|
-
provider: input.provider,
|
|
383
|
+
provider: normalizeProviderConfig(input.provider),
|
|
344
384
|
output: normalizeOutputConfig(input.output),
|
|
345
385
|
naming: {
|
|
346
386
|
...DEFAULT_NAMING,
|
|
@@ -550,12 +590,19 @@ export const ${registryConstName} = defineRegistry({
|
|
|
550
590
|
};
|
|
551
591
|
}
|
|
552
592
|
function assertNoDuplicatePaths(files) {
|
|
553
|
-
const seen = /* @__PURE__ */ new
|
|
593
|
+
const seen = /* @__PURE__ */ new Map();
|
|
554
594
|
for (const file of files) {
|
|
555
|
-
|
|
556
|
-
|
|
595
|
+
const existing = seen.get(file.path);
|
|
596
|
+
if (existing) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
[
|
|
599
|
+
`Generator output collision detected for path: ${file.path}`,
|
|
600
|
+
`Collision: ${existing.kind} and ${file.kind}.`,
|
|
601
|
+
"When syncing multiple schemas, include a schema placeholder such as {schema} or {schema_kebab} in model/schema output targets."
|
|
602
|
+
].join(" ")
|
|
603
|
+
);
|
|
557
604
|
}
|
|
558
|
-
seen.
|
|
605
|
+
seen.set(file.path, file);
|
|
559
606
|
}
|
|
560
607
|
}
|
|
561
608
|
var ArtifactComposer = class {
|
|
@@ -2059,8 +2106,73 @@ function toTypeKind(code) {
|
|
|
2059
2106
|
function escapeSqlLiteral(value) {
|
|
2060
2107
|
return value.replace(/'/g, "''");
|
|
2061
2108
|
}
|
|
2109
|
+
function parsePostgresArrayLiteral(text) {
|
|
2110
|
+
const body = text.slice(1, -1).trim();
|
|
2111
|
+
if (!body) return [];
|
|
2112
|
+
const values = [];
|
|
2113
|
+
let current = "";
|
|
2114
|
+
let inQuotes = false;
|
|
2115
|
+
let escaped = false;
|
|
2116
|
+
for (const char of body) {
|
|
2117
|
+
if (escaped) {
|
|
2118
|
+
current += char;
|
|
2119
|
+
escaped = false;
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
if (char === "\\") {
|
|
2123
|
+
escaped = true;
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
if (char === '"') {
|
|
2127
|
+
inQuotes = !inQuotes;
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
if (char === "," && !inQuotes) {
|
|
2131
|
+
values.push(current);
|
|
2132
|
+
current = "";
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
current += char;
|
|
2136
|
+
}
|
|
2137
|
+
values.push(current);
|
|
2138
|
+
return values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
2139
|
+
}
|
|
2140
|
+
function coerceStringArray(value) {
|
|
2141
|
+
if (Array.isArray(value)) {
|
|
2142
|
+
return value.map((item) => typeof item === "string" ? item : String(item)).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
2143
|
+
}
|
|
2144
|
+
if (typeof value === "string") {
|
|
2145
|
+
const trimmed = value.trim();
|
|
2146
|
+
if (!trimmed) return [];
|
|
2147
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2148
|
+
try {
|
|
2149
|
+
const parsed = JSON.parse(trimmed);
|
|
2150
|
+
return coerceStringArray(parsed);
|
|
2151
|
+
} catch {
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
2155
|
+
return parsePostgresArrayLiteral(trimmed);
|
|
2156
|
+
}
|
|
2157
|
+
return trimmed.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
2158
|
+
}
|
|
2159
|
+
return [];
|
|
2160
|
+
}
|
|
2161
|
+
function normalizePostgresCatalogSchemas(schemas) {
|
|
2162
|
+
const normalized = [];
|
|
2163
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2164
|
+
for (const value of schemas ?? []) {
|
|
2165
|
+
const schema = value.trim();
|
|
2166
|
+
if (!schema || seen.has(schema)) {
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
seen.add(schema);
|
|
2170
|
+
normalized.push(schema);
|
|
2171
|
+
}
|
|
2172
|
+
return normalized.length > 0 ? normalized : ["public"];
|
|
2173
|
+
}
|
|
2062
2174
|
function buildSchemaArrayLiteral(schemas) {
|
|
2063
|
-
const normalized = schemas
|
|
2175
|
+
const normalized = normalizePostgresCatalogSchemas(schemas);
|
|
2064
2176
|
const literals = normalized.map((schema) => `'${escapeSqlLiteral(schema)}'`).join(", ");
|
|
2065
2177
|
return `ARRAY[${literals}]`;
|
|
2066
2178
|
}
|
|
@@ -2098,8 +2210,10 @@ var PostgresCatalogSnapshotAssembler = class {
|
|
|
2098
2210
|
addPrimaryKeyRows(primaryKeyRows) {
|
|
2099
2211
|
for (const row of primaryKeyRows) {
|
|
2100
2212
|
const table = this.ensureTable(row.schema_name, row.table_name);
|
|
2101
|
-
|
|
2102
|
-
|
|
2213
|
+
const primaryKeyColumns = coerceStringArray(row.columns);
|
|
2214
|
+
row.columns = primaryKeyColumns;
|
|
2215
|
+
table.primaryKey = primaryKeyColumns;
|
|
2216
|
+
for (const columnName of primaryKeyColumns) {
|
|
2103
2217
|
const column = table.columns[columnName];
|
|
2104
2218
|
if (column) {
|
|
2105
2219
|
column.isPrimaryKey = true;
|
|
@@ -2111,23 +2225,27 @@ var PostgresCatalogSnapshotAssembler = class {
|
|
|
2111
2225
|
for (const row of foreignKeyRows) {
|
|
2112
2226
|
const sourceTable = this.ensureTable(row.source_schema, row.source_table);
|
|
2113
2227
|
const targetTable = this.ensureTable(row.target_schema, row.target_table);
|
|
2228
|
+
const sourceColumns = coerceStringArray(row.source_columns);
|
|
2229
|
+
const targetColumns = coerceStringArray(row.target_columns);
|
|
2230
|
+
row.source_columns = sourceColumns;
|
|
2231
|
+
row.target_columns = targetColumns;
|
|
2114
2232
|
const sourceRelationKind = row.source_is_unique ? "one-to-one" : "many-to-one";
|
|
2115
2233
|
this.upsertRelation(sourceTable, relationKey(row.constraint_name, row.target_table), {
|
|
2116
2234
|
name: row.constraint_name,
|
|
2117
2235
|
kind: sourceRelationKind,
|
|
2118
|
-
sourceColumns
|
|
2236
|
+
sourceColumns,
|
|
2119
2237
|
targetSchema: row.target_schema,
|
|
2120
2238
|
targetModel: row.target_table,
|
|
2121
|
-
targetColumns
|
|
2239
|
+
targetColumns
|
|
2122
2240
|
});
|
|
2123
2241
|
const targetRelationKind = row.source_is_unique ? "one-to-one" : "one-to-many";
|
|
2124
2242
|
this.upsertRelation(targetTable, relationKey(row.source_table), {
|
|
2125
2243
|
name: relationKey(row.source_table, row.constraint_name),
|
|
2126
2244
|
kind: targetRelationKind,
|
|
2127
|
-
sourceColumns:
|
|
2245
|
+
sourceColumns: targetColumns,
|
|
2128
2246
|
targetSchema: row.source_schema,
|
|
2129
2247
|
targetModel: row.source_table,
|
|
2130
|
-
targetColumns:
|
|
2248
|
+
targetColumns: sourceColumns
|
|
2131
2249
|
});
|
|
2132
2250
|
}
|
|
2133
2251
|
}
|
|
@@ -2253,12 +2371,14 @@ var PostgresIntrospectionProvider = class {
|
|
|
2253
2371
|
backend = "postgresql";
|
|
2254
2372
|
connectionString;
|
|
2255
2373
|
database;
|
|
2374
|
+
schemas;
|
|
2256
2375
|
constructor(options) {
|
|
2257
2376
|
this.connectionString = options.connectionString;
|
|
2258
2377
|
this.database = options.database ?? "postgres";
|
|
2378
|
+
this.schemas = normalizePostgresCatalogSchemas(options.schemas);
|
|
2259
2379
|
}
|
|
2260
2380
|
async inspect(options) {
|
|
2261
|
-
const schemas = options?.schemas && options.schemas.length > 0 ? options.schemas :
|
|
2381
|
+
const schemas = options?.schemas && options.schemas.length > 0 ? normalizePostgresCatalogSchemas(options.schemas) : this.schemas;
|
|
2262
2382
|
const pool = new pg.Pool({
|
|
2263
2383
|
connectionString: this.connectionString
|
|
2264
2384
|
});
|
|
@@ -2330,11 +2450,13 @@ var AthenaGatewayPostgresIntrospectionProvider = class {
|
|
|
2330
2450
|
type: this.config.backend ?? "postgresql"
|
|
2331
2451
|
}
|
|
2332
2452
|
});
|
|
2453
|
+
this.schemas = normalizeSchemaSelection(this.config.schemas);
|
|
2333
2454
|
}
|
|
2334
2455
|
backend = "postgresql";
|
|
2335
2456
|
client;
|
|
2457
|
+
schemas;
|
|
2336
2458
|
async inspect(options) {
|
|
2337
|
-
const schemas = options?.schemas && options.schemas.length > 0 ? options.schemas : this.
|
|
2459
|
+
const schemas = options?.schemas && options.schemas.length > 0 ? normalizeSchemaSelection(options.schemas) : this.schemas;
|
|
2338
2460
|
const catalogClient = new AthenaGatewayCatalogClient(this.client);
|
|
2339
2461
|
const queries = buildGatewayCatalogQueries(schemas);
|
|
2340
2462
|
const [columnRows, enumMap, primaryKeyRows, foreignKeyRows] = await Promise.all([
|
|
@@ -2370,7 +2492,8 @@ var ScyllaIntrospectionProvider = class {
|
|
|
2370
2492
|
function createPostgresProvider(config) {
|
|
2371
2493
|
return createPostgresIntrospectionProvider({
|
|
2372
2494
|
connectionString: config.connectionString,
|
|
2373
|
-
database: config.database
|
|
2495
|
+
database: config.database,
|
|
2496
|
+
schemas: normalizeSchemaSelection(config.schemas)
|
|
2374
2497
|
});
|
|
2375
2498
|
}
|
|
2376
2499
|
function resolveGeneratorProvider(providerConfig, experimentalFlags) {
|
|
@@ -2392,12 +2515,6 @@ function resolveGeneratorProvider(providerConfig, experimentalFlags) {
|
|
|
2392
2515
|
}
|
|
2393
2516
|
|
|
2394
2517
|
// src/generator/pipeline.ts
|
|
2395
|
-
function extractProviderSchemas(providerConfig) {
|
|
2396
|
-
if (!("schemas" in providerConfig) || !providerConfig.schemas || providerConfig.schemas.length === 0) {
|
|
2397
|
-
return void 0;
|
|
2398
|
-
}
|
|
2399
|
-
return providerConfig.schemas;
|
|
2400
|
-
}
|
|
2401
2518
|
async function writeArtifacts(files, cwd) {
|
|
2402
2519
|
const writtenFiles = [];
|
|
2403
2520
|
for (const file of files) {
|
|
@@ -2417,7 +2534,7 @@ async function runSchemaGenerator(options = {}) {
|
|
|
2417
2534
|
const { configPath, config } = await loadGeneratorConfig(configOptions);
|
|
2418
2535
|
const provider = options.provider ?? resolveGeneratorProvider(config.provider, config.experimental);
|
|
2419
2536
|
const snapshot = await provider.inspect({
|
|
2420
|
-
schemas:
|
|
2537
|
+
schemas: resolveProviderSchemas(config.provider)
|
|
2421
2538
|
});
|
|
2422
2539
|
const generated = generateArtifactsFromSnapshot(snapshot, config);
|
|
2423
2540
|
const writtenFiles = options.dryRun ? [] : await writeArtifacts(generated.files, cwd);
|
|
@@ -2429,7 +2546,7 @@ async function runSchemaGenerator(options = {}) {
|
|
|
2429
2546
|
}
|
|
2430
2547
|
|
|
2431
2548
|
// src/cli/index.ts
|
|
2432
|
-
function
|
|
2549
|
+
function rootUsage() {
|
|
2433
2550
|
return [
|
|
2434
2551
|
"athena-js CLI",
|
|
2435
2552
|
"",
|
|
@@ -2438,12 +2555,42 @@ function usage() {
|
|
|
2438
2555
|
"",
|
|
2439
2556
|
"Examples:",
|
|
2440
2557
|
" athena-js generate",
|
|
2558
|
+
" athena-js generate --config ./athena.config.ts --dry-run",
|
|
2559
|
+
" athena-js generate --help"
|
|
2560
|
+
].join("\n");
|
|
2561
|
+
}
|
|
2562
|
+
function generateUsage() {
|
|
2563
|
+
return [
|
|
2564
|
+
"athena-js generate",
|
|
2565
|
+
"",
|
|
2566
|
+
"Usage:",
|
|
2567
|
+
" athena-js generate [--config <path>] [--dry-run]",
|
|
2568
|
+
"",
|
|
2569
|
+
"Options:",
|
|
2570
|
+
" --config <path> Explicit path to athena.config.ts or athena-js.config.ts",
|
|
2571
|
+
" --dry-run Build generated files in memory without writing them to disk",
|
|
2572
|
+
" -h, --help Show help for generate",
|
|
2573
|
+
"",
|
|
2574
|
+
"Examples:",
|
|
2575
|
+
" athena-js generate",
|
|
2441
2576
|
" athena-js generate --config ./athena.config.ts --dry-run"
|
|
2442
2577
|
].join("\n");
|
|
2443
2578
|
}
|
|
2579
|
+
function usage(topic = "root") {
|
|
2580
|
+
return topic === "generate" ? generateUsage() : rootUsage();
|
|
2581
|
+
}
|
|
2444
2582
|
function parseCommand(argv) {
|
|
2445
|
-
if (argv.length === 0 || argv[0] === "
|
|
2446
|
-
return { command: "help" };
|
|
2583
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
2584
|
+
return { command: "help", topic: "root" };
|
|
2585
|
+
}
|
|
2586
|
+
if (argv[0] === "help") {
|
|
2587
|
+
if (argv.length === 1) {
|
|
2588
|
+
return { command: "help", topic: "root" };
|
|
2589
|
+
}
|
|
2590
|
+
if (argv[1] === "generate") {
|
|
2591
|
+
return { command: "help", topic: "generate" };
|
|
2592
|
+
}
|
|
2593
|
+
throw new Error(`Unknown command "${argv[1]}".`);
|
|
2447
2594
|
}
|
|
2448
2595
|
const [command, ...rest] = argv;
|
|
2449
2596
|
if (command !== "generate") {
|
|
@@ -2453,13 +2600,16 @@ function parseCommand(argv) {
|
|
|
2453
2600
|
let dryRun = false;
|
|
2454
2601
|
for (let index = 0; index < rest.length; index += 1) {
|
|
2455
2602
|
const token = rest[index];
|
|
2603
|
+
if (token === "--help" || token === "-h") {
|
|
2604
|
+
return { command: "help", topic: "generate" };
|
|
2605
|
+
}
|
|
2456
2606
|
if (token === "--dry-run") {
|
|
2457
2607
|
dryRun = true;
|
|
2458
2608
|
continue;
|
|
2459
2609
|
}
|
|
2460
2610
|
if (token === "--config") {
|
|
2461
2611
|
const nextValue = rest[index + 1];
|
|
2462
|
-
if (!nextValue) {
|
|
2612
|
+
if (!nextValue || nextValue.startsWith("-")) {
|
|
2463
2613
|
throw new Error("Missing value for --config option.");
|
|
2464
2614
|
}
|
|
2465
2615
|
configPath = nextValue;
|
|
@@ -2474,29 +2624,72 @@ function parseCommand(argv) {
|
|
|
2474
2624
|
dryRun
|
|
2475
2625
|
};
|
|
2476
2626
|
}
|
|
2477
|
-
|
|
2627
|
+
function normalizeErrorMessage(error) {
|
|
2628
|
+
if (error instanceof Error) {
|
|
2629
|
+
return error.message;
|
|
2630
|
+
}
|
|
2631
|
+
if (typeof error === "string") {
|
|
2632
|
+
return error;
|
|
2633
|
+
}
|
|
2634
|
+
return "Unknown generator error.";
|
|
2635
|
+
}
|
|
2636
|
+
function extractMissingDatabaseName(message) {
|
|
2637
|
+
const match = message.match(/database "([^"]+)" does not exist/i);
|
|
2638
|
+
return match?.[1];
|
|
2639
|
+
}
|
|
2640
|
+
function isErrorWithCode(error) {
|
|
2641
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
2642
|
+
}
|
|
2643
|
+
function formatGeneratorError(error, configPath) {
|
|
2644
|
+
if (isErrorWithCode(error) && error.code === "3D000") {
|
|
2645
|
+
const message = normalizeErrorMessage(error);
|
|
2646
|
+
const databaseName = extractMissingDatabaseName(message);
|
|
2647
|
+
const databaseLabel = databaseName ? `PostgreSQL database "${databaseName}" does not exist` : "The target PostgreSQL database does not exist";
|
|
2648
|
+
const configLabel = configPath ? `config "${configPath}"` : "the resolved athena config";
|
|
2649
|
+
return new Error(
|
|
2650
|
+
[
|
|
2651
|
+
`${databaseLabel} (code 3D000).`,
|
|
2652
|
+
`Update provider.connectionString (and provider.database, if set) in ${configLabel}, or create that database before running generate.`
|
|
2653
|
+
].join("\n")
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
if (error instanceof Error) {
|
|
2657
|
+
return error;
|
|
2658
|
+
}
|
|
2659
|
+
return new Error(normalizeErrorMessage(error));
|
|
2660
|
+
}
|
|
2661
|
+
async function runCLI(argv, runtime = {}) {
|
|
2662
|
+
const log = runtime.log ?? console.log;
|
|
2663
|
+
const runGenerator = runtime.runGenerator ?? runSchemaGenerator;
|
|
2478
2664
|
const parsed = parseCommand(argv);
|
|
2479
2665
|
if (parsed.command === "help") {
|
|
2480
|
-
|
|
2666
|
+
log(usage(parsed.topic));
|
|
2481
2667
|
return;
|
|
2482
2668
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2669
|
+
let result;
|
|
2670
|
+
try {
|
|
2671
|
+
result = await runGenerator({
|
|
2672
|
+
configPath: parsed.configPath,
|
|
2673
|
+
dryRun: parsed.dryRun
|
|
2674
|
+
});
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
throw formatGeneratorError(error, parsed.configPath);
|
|
2677
|
+
}
|
|
2487
2678
|
if (parsed.dryRun) {
|
|
2488
|
-
|
|
2679
|
+
log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
|
|
2489
2680
|
for (const file of result.files) {
|
|
2490
|
-
|
|
2681
|
+
log(` - ${file.path}`);
|
|
2491
2682
|
}
|
|
2492
2683
|
return;
|
|
2493
2684
|
}
|
|
2494
|
-
|
|
2685
|
+
log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
|
|
2495
2686
|
for (const filePath of result.writtenFiles) {
|
|
2496
|
-
|
|
2687
|
+
log(` - ${filePath}`);
|
|
2497
2688
|
}
|
|
2498
2689
|
}
|
|
2499
2690
|
|
|
2691
|
+
exports.parseCommand = parseCommand;
|
|
2500
2692
|
exports.runCLI = runCLI;
|
|
2693
|
+
exports.usage = usage;
|
|
2501
2694
|
//# sourceMappingURL=index.cjs.map
|
|
2502
2695
|
//# sourceMappingURL=index.cjs.map
|