@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # athena-js
2
2
 
3
- current version: `1.6.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 (phase 2 scaffolding)
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
- The SDK now includes a project-root generator config system (`athena.config.ts`) and CLI command:
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 output paths support placeholder tokens (database/schema/model + case variants), feature flags, and experimental provider contracts.
97
- PostgreSQL introspection works both via direct `pg_url` and gateway-only `/gateway/query` execution.
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('../dist/cli/index.js')
4
- .then(({ runCLI }) => runCLI(process.argv.slice(2)))
5
- .catch(err => {
6
- console.error('Failed to start athena-js CLI:', err)
7
- process.exit(1)
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()
@@ -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: "src/generated/{database_kebab}/{schema_kebab}/{model_kebab}.model.ts",
304
- schema: "src/generated/{database_kebab}/{schema_kebab}/index.ts",
305
- database: "src/generated/{database_kebab}/index.ts",
306
- registry: "src/generated/index.ts"
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 Set();
593
+ const seen = /* @__PURE__ */ new Map();
554
594
  for (const file of files) {
555
- if (seen.has(file.path)) {
556
- throw new Error(`Generator output collision detected for path: ${file.path}`);
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.add(file.path);
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.length > 0 ? schemas : ["public"];
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
- table.primaryKey = row.columns;
2102
- for (const columnName of row.columns) {
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: row.source_columns,
2236
+ sourceColumns,
2119
2237
  targetSchema: row.target_schema,
2120
2238
  targetModel: row.target_table,
2121
- targetColumns: row.target_columns
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: row.target_columns,
2245
+ sourceColumns: targetColumns,
2128
2246
  targetSchema: row.source_schema,
2129
2247
  targetModel: row.source_table,
2130
- targetColumns: row.source_columns
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 : ["public"];
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.config.schemas && this.config.schemas.length > 0 ? this.config.schemas : ["public"];
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: extractProviderSchemas(config.provider)
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 usage() {
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] === "help" || argv[0] === "--help" || argv[0] === "-h") {
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
- async function runCLI(argv) {
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
- console.log(usage());
2666
+ log(usage(parsed.topic));
2481
2667
  return;
2482
2668
  }
2483
- const result = await runSchemaGenerator({
2484
- configPath: parsed.configPath,
2485
- dryRun: parsed.dryRun
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
- console.log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
2679
+ log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
2489
2680
  for (const file of result.files) {
2490
- console.log(` - ${file.path}`);
2681
+ log(` - ${file.path}`);
2491
2682
  }
2492
2683
  return;
2493
2684
  }
2494
- console.log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
2685
+ log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
2495
2686
  for (const filePath of result.writtenFiles) {
2496
- console.log(` - ${filePath}`);
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