@xylex-group/athena 1.6.1 → 1.6.2

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.6.2`
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
@@ -83,22 +83,45 @@ await typed
83
83
 
84
84
  For full details, see [`docs/typed-schema-registry.md`](./docs/typed-schema-registry.md).
85
85
 
86
- ### Typed schema generator (phase 2 scaffolding)
86
+ ### Typed schema generator
87
87
 
88
- The SDK now includes a project-root generator config system (`athena.config.ts`) and CLI command:
88
+ Schema generation is additive. Existing `createClient(...).from<T>(...)` usage remains valid while teams migrate to generated registry files.
89
+
90
+ CLI:
89
91
 
90
92
  ```bash
91
93
  athena-js generate
92
94
  athena-js generate --dry-run
93
95
  athena-js generate --config ./athena.config.ts
96
+ athena-js generate --help
97
+ athena-js help generate
94
98
  ```
95
99
 
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.
100
+ Generator supports:
101
+
102
+ - PostgreSQL direct introspection (`provider.mode = "direct"`, `provider.connectionString` from your `PG_URL`/`DATABASE_URL`)
103
+ - PostgreSQL gateway-only introspection (`provider.mode = "gateway"` via Athena `POST /gateway/query`)
104
+ - Placeholder-driven output paths
105
+ - Feature flags (`features.emitRegistry`, `features.emitRelations`)
98
106
 
99
- For full generator configuration, see [`docs/generator-config.md`](./docs/generator-config.md).
107
+ For full generator configuration and troubleshooting, see [`docs/generator-config.md`](./docs/generator-config.md).
108
+ For full CLI commands, help behavior, and troubleshooting, see [`docs/cli-command-reference.md`](./docs/cli-command-reference.md).
109
+ For CI/CD pipelines and generated-file branch policy, see [`docs/generator-cicd.md`](./docs/generator-cicd.md).
100
110
  For prompt-ready documentation handoff text, see [`docs/generator-codex-handoff-prompt-pack.md`](./docs/generator-codex-handoff-prompt-pack.md).
101
111
 
112
+ ### Athena JS and Athena RS
113
+
114
+ `athena-js` is designed to be standalone for TypeScript/Node and React-native workflows:
115
+
116
+ - query builder + hooks
117
+ - typed registry and generator pipeline
118
+ - CLI-driven codegen in JS/TS projects
119
+
120
+ `athena-rs` remains the faster fit for Rust service execution paths. Teams can run both in parallel:
121
+
122
+ - `athena-rs` for Rust backend throughput
123
+ - `athena-js` for app/tooling layers that need TypeScript contracts and frontend-facing ergonomics
124
+
102
125
  Every query resolves to `{ data, error, errorDetails?, status, count?, raw }`. `data` is `null` on error; `error` is `null` on success.
103
126
 
104
127
  For richer handling, inspect `errorDetails` (`code`, `status`, `endpoint`, `method`, `requestId`, etc.) or use `AthenaGatewayError` / `isAthenaGatewayError` from the package exports.
@@ -666,6 +689,8 @@ CI and publish workflows run `typecheck` before build/publish.
666
689
 
667
690
  - [Documentation index](docs/index.md) — complete documentation map
668
691
  - [Getting started](docs/getting-started.md) — step-by-step walkthrough
692
+ - [CLI command reference](docs/cli-command-reference.md) — all `athena-js` commands, help flows, and troubleshooting
669
693
  - [Typed schema registry](docs/typed-schema-registry.md) — typed contracts and migration path
670
694
  - [Generator config](docs/generator-config.md) — generator provider and output pipeline
695
+ - [Generator CI/CD](docs/generator-cicd.md) — pipeline patterns, secret mapping, retries, and branch policy
671
696
  - [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()
@@ -300,10 +300,10 @@ var DEFAULT_CONFIG_CANDIDATES = [
300
300
  ".athena.config.js"
301
301
  ];
302
302
  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"
303
+ model: "athena/models/{model_kebab}.ts",
304
+ schema: "athena/schema.ts",
305
+ database: "athena/relations.ts",
306
+ registry: "athena/config.ts"
307
307
  };
308
308
  var DEFAULT_NAMING = {
309
309
  modelType: "pascal",
@@ -2059,6 +2059,58 @@ function toTypeKind(code) {
2059
2059
  function escapeSqlLiteral(value) {
2060
2060
  return value.replace(/'/g, "''");
2061
2061
  }
2062
+ function parsePostgresArrayLiteral(text) {
2063
+ const body = text.slice(1, -1).trim();
2064
+ if (!body) return [];
2065
+ const values = [];
2066
+ let current = "";
2067
+ let inQuotes = false;
2068
+ let escaped = false;
2069
+ for (const char of body) {
2070
+ if (escaped) {
2071
+ current += char;
2072
+ escaped = false;
2073
+ continue;
2074
+ }
2075
+ if (char === "\\") {
2076
+ escaped = true;
2077
+ continue;
2078
+ }
2079
+ if (char === '"') {
2080
+ inQuotes = !inQuotes;
2081
+ continue;
2082
+ }
2083
+ if (char === "," && !inQuotes) {
2084
+ values.push(current);
2085
+ current = "";
2086
+ continue;
2087
+ }
2088
+ current += char;
2089
+ }
2090
+ values.push(current);
2091
+ return values.map((value) => value.trim()).filter((value) => value.length > 0);
2092
+ }
2093
+ function coerceStringArray(value) {
2094
+ if (Array.isArray(value)) {
2095
+ return value.map((item) => typeof item === "string" ? item : String(item)).map((item) => item.trim()).filter((item) => item.length > 0);
2096
+ }
2097
+ if (typeof value === "string") {
2098
+ const trimmed = value.trim();
2099
+ if (!trimmed) return [];
2100
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
2101
+ try {
2102
+ const parsed = JSON.parse(trimmed);
2103
+ return coerceStringArray(parsed);
2104
+ } catch {
2105
+ }
2106
+ }
2107
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
2108
+ return parsePostgresArrayLiteral(trimmed);
2109
+ }
2110
+ return trimmed.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
2111
+ }
2112
+ return [];
2113
+ }
2062
2114
  function buildSchemaArrayLiteral(schemas) {
2063
2115
  const normalized = schemas.length > 0 ? schemas : ["public"];
2064
2116
  const literals = normalized.map((schema) => `'${escapeSqlLiteral(schema)}'`).join(", ");
@@ -2098,8 +2150,10 @@ var PostgresCatalogSnapshotAssembler = class {
2098
2150
  addPrimaryKeyRows(primaryKeyRows) {
2099
2151
  for (const row of primaryKeyRows) {
2100
2152
  const table = this.ensureTable(row.schema_name, row.table_name);
2101
- table.primaryKey = row.columns;
2102
- for (const columnName of row.columns) {
2153
+ const primaryKeyColumns = coerceStringArray(row.columns);
2154
+ row.columns = primaryKeyColumns;
2155
+ table.primaryKey = primaryKeyColumns;
2156
+ for (const columnName of primaryKeyColumns) {
2103
2157
  const column = table.columns[columnName];
2104
2158
  if (column) {
2105
2159
  column.isPrimaryKey = true;
@@ -2111,23 +2165,27 @@ var PostgresCatalogSnapshotAssembler = class {
2111
2165
  for (const row of foreignKeyRows) {
2112
2166
  const sourceTable = this.ensureTable(row.source_schema, row.source_table);
2113
2167
  const targetTable = this.ensureTable(row.target_schema, row.target_table);
2168
+ const sourceColumns = coerceStringArray(row.source_columns);
2169
+ const targetColumns = coerceStringArray(row.target_columns);
2170
+ row.source_columns = sourceColumns;
2171
+ row.target_columns = targetColumns;
2114
2172
  const sourceRelationKind = row.source_is_unique ? "one-to-one" : "many-to-one";
2115
2173
  this.upsertRelation(sourceTable, relationKey(row.constraint_name, row.target_table), {
2116
2174
  name: row.constraint_name,
2117
2175
  kind: sourceRelationKind,
2118
- sourceColumns: row.source_columns,
2176
+ sourceColumns,
2119
2177
  targetSchema: row.target_schema,
2120
2178
  targetModel: row.target_table,
2121
- targetColumns: row.target_columns
2179
+ targetColumns
2122
2180
  });
2123
2181
  const targetRelationKind = row.source_is_unique ? "one-to-one" : "one-to-many";
2124
2182
  this.upsertRelation(targetTable, relationKey(row.source_table), {
2125
2183
  name: relationKey(row.source_table, row.constraint_name),
2126
2184
  kind: targetRelationKind,
2127
- sourceColumns: row.target_columns,
2185
+ sourceColumns: targetColumns,
2128
2186
  targetSchema: row.source_schema,
2129
2187
  targetModel: row.source_table,
2130
- targetColumns: row.source_columns
2188
+ targetColumns: sourceColumns
2131
2189
  });
2132
2190
  }
2133
2191
  }
@@ -2429,7 +2487,7 @@ async function runSchemaGenerator(options = {}) {
2429
2487
  }
2430
2488
 
2431
2489
  // src/cli/index.ts
2432
- function usage() {
2490
+ function rootUsage() {
2433
2491
  return [
2434
2492
  "athena-js CLI",
2435
2493
  "",
@@ -2438,12 +2496,42 @@ function usage() {
2438
2496
  "",
2439
2497
  "Examples:",
2440
2498
  " athena-js generate",
2499
+ " athena-js generate --config ./athena.config.ts --dry-run",
2500
+ " athena-js generate --help"
2501
+ ].join("\n");
2502
+ }
2503
+ function generateUsage() {
2504
+ return [
2505
+ "athena-js generate",
2506
+ "",
2507
+ "Usage:",
2508
+ " athena-js generate [--config <path>] [--dry-run]",
2509
+ "",
2510
+ "Options:",
2511
+ " --config <path> Explicit path to athena.config.ts or athena-js.config.ts",
2512
+ " --dry-run Build generated files in memory without writing them to disk",
2513
+ " -h, --help Show help for generate",
2514
+ "",
2515
+ "Examples:",
2516
+ " athena-js generate",
2441
2517
  " athena-js generate --config ./athena.config.ts --dry-run"
2442
2518
  ].join("\n");
2443
2519
  }
2520
+ function usage(topic = "root") {
2521
+ return topic === "generate" ? generateUsage() : rootUsage();
2522
+ }
2444
2523
  function parseCommand(argv) {
2445
- if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
2446
- return { command: "help" };
2524
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
2525
+ return { command: "help", topic: "root" };
2526
+ }
2527
+ if (argv[0] === "help") {
2528
+ if (argv.length === 1) {
2529
+ return { command: "help", topic: "root" };
2530
+ }
2531
+ if (argv[1] === "generate") {
2532
+ return { command: "help", topic: "generate" };
2533
+ }
2534
+ throw new Error(`Unknown command "${argv[1]}".`);
2447
2535
  }
2448
2536
  const [command, ...rest] = argv;
2449
2537
  if (command !== "generate") {
@@ -2453,13 +2541,16 @@ function parseCommand(argv) {
2453
2541
  let dryRun = false;
2454
2542
  for (let index = 0; index < rest.length; index += 1) {
2455
2543
  const token = rest[index];
2544
+ if (token === "--help" || token === "-h") {
2545
+ return { command: "help", topic: "generate" };
2546
+ }
2456
2547
  if (token === "--dry-run") {
2457
2548
  dryRun = true;
2458
2549
  continue;
2459
2550
  }
2460
2551
  if (token === "--config") {
2461
2552
  const nextValue = rest[index + 1];
2462
- if (!nextValue) {
2553
+ if (!nextValue || nextValue.startsWith("-")) {
2463
2554
  throw new Error("Missing value for --config option.");
2464
2555
  }
2465
2556
  configPath = nextValue;
@@ -2474,29 +2565,72 @@ function parseCommand(argv) {
2474
2565
  dryRun
2475
2566
  };
2476
2567
  }
2477
- async function runCLI(argv) {
2568
+ function normalizeErrorMessage(error) {
2569
+ if (error instanceof Error) {
2570
+ return error.message;
2571
+ }
2572
+ if (typeof error === "string") {
2573
+ return error;
2574
+ }
2575
+ return "Unknown generator error.";
2576
+ }
2577
+ function extractMissingDatabaseName(message) {
2578
+ const match = message.match(/database "([^"]+)" does not exist/i);
2579
+ return match?.[1];
2580
+ }
2581
+ function isErrorWithCode(error) {
2582
+ return typeof error === "object" && error !== null && "code" in error;
2583
+ }
2584
+ function formatGeneratorError(error, configPath) {
2585
+ if (isErrorWithCode(error) && error.code === "3D000") {
2586
+ const message = normalizeErrorMessage(error);
2587
+ const databaseName = extractMissingDatabaseName(message);
2588
+ const databaseLabel = databaseName ? `PostgreSQL database "${databaseName}" does not exist` : "The target PostgreSQL database does not exist";
2589
+ const configLabel = configPath ? `config "${configPath}"` : "the resolved athena config";
2590
+ return new Error(
2591
+ [
2592
+ `${databaseLabel} (code 3D000).`,
2593
+ `Update provider.connectionString (and provider.database, if set) in ${configLabel}, or create that database before running generate.`
2594
+ ].join("\n")
2595
+ );
2596
+ }
2597
+ if (error instanceof Error) {
2598
+ return error;
2599
+ }
2600
+ return new Error(normalizeErrorMessage(error));
2601
+ }
2602
+ async function runCLI(argv, runtime = {}) {
2603
+ const log = runtime.log ?? console.log;
2604
+ const runGenerator = runtime.runGenerator ?? runSchemaGenerator;
2478
2605
  const parsed = parseCommand(argv);
2479
2606
  if (parsed.command === "help") {
2480
- console.log(usage());
2607
+ log(usage(parsed.topic));
2481
2608
  return;
2482
2609
  }
2483
- const result = await runSchemaGenerator({
2484
- configPath: parsed.configPath,
2485
- dryRun: parsed.dryRun
2486
- });
2610
+ let result;
2611
+ try {
2612
+ result = await runGenerator({
2613
+ configPath: parsed.configPath,
2614
+ dryRun: parsed.dryRun
2615
+ });
2616
+ } catch (error) {
2617
+ throw formatGeneratorError(error, parsed.configPath);
2618
+ }
2487
2619
  if (parsed.dryRun) {
2488
- console.log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
2620
+ log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
2489
2621
  for (const file of result.files) {
2490
- console.log(` - ${file.path}`);
2622
+ log(` - ${file.path}`);
2491
2623
  }
2492
2624
  return;
2493
2625
  }
2494
- console.log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
2626
+ log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
2495
2627
  for (const filePath of result.writtenFiles) {
2496
- console.log(` - ${filePath}`);
2628
+ log(` - ${filePath}`);
2497
2629
  }
2498
2630
  }
2499
2631
 
2632
+ exports.parseCommand = parseCommand;
2500
2633
  exports.runCLI = runCLI;
2634
+ exports.usage = usage;
2501
2635
  //# sourceMappingURL=index.cjs.map
2502
2636
  //# sourceMappingURL=index.cjs.map