@xylex-group/athena 1.6.0 → 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.0`
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",
@@ -331,6 +331,13 @@ function normalizeOutputConfig(output) {
331
331
  }
332
332
  };
333
333
  }
334
+ function isAthenaGeneratorConfig(value) {
335
+ if (!value || typeof value !== "object") {
336
+ return false;
337
+ }
338
+ const record = value;
339
+ return Boolean(record.provider && typeof record.provider === "object") && Boolean(record.output && typeof record.output === "object");
340
+ }
334
341
  function normalizeGeneratorConfig(input) {
335
342
  return {
336
343
  provider: input.provider,
@@ -359,18 +366,41 @@ function findGeneratorConfigPath(cwd = process.cwd()) {
359
366
  return void 0;
360
367
  }
361
368
  function extractConfigExport(module) {
362
- if (module && typeof module === "object") {
363
- const record = module;
369
+ const visited = /* @__PURE__ */ new Set();
370
+ const queue = [module];
371
+ while (queue.length > 0) {
372
+ const current = queue.shift();
373
+ if (!current || typeof current !== "object" || visited.has(current)) {
374
+ continue;
375
+ }
376
+ visited.add(current);
377
+ const record = current;
378
+ if (isAthenaGeneratorConfig(record)) {
379
+ return record;
380
+ }
364
381
  const defaultExport = record.default;
365
382
  if (defaultExport && typeof defaultExport === "object") {
366
- return defaultExport;
383
+ queue.push(defaultExport);
367
384
  }
368
385
  const namedConfigExport = record.config;
369
386
  if (namedConfigExport && typeof namedConfigExport === "object") {
370
- return namedConfigExport;
387
+ queue.push(namedConfigExport);
388
+ }
389
+ const moduleExports = record["module.exports"];
390
+ if (moduleExports && typeof moduleExports === "object") {
391
+ queue.push(moduleExports);
371
392
  }
372
393
  }
373
- throw new Error("Generator config file must export a config object as default export or `config`.");
394
+ throw new Error(
395
+ "Generator config file must export a config object as default export or `config`."
396
+ );
397
+ }
398
+ function importConfigModule(moduleSpecifier) {
399
+ const runtimeImport = new Function(
400
+ "moduleSpecifier",
401
+ "return import(moduleSpecifier)"
402
+ );
403
+ return runtimeImport(moduleSpecifier);
374
404
  }
375
405
  async function loadGeneratorConfig(options = {}) {
376
406
  const cwd = options.cwd ?? process.cwd();
@@ -381,7 +411,7 @@ async function loadGeneratorConfig(options = {}) {
381
411
  );
382
412
  }
383
413
  const moduleUrl = url.pathToFileURL(resolvedPath);
384
- const module = await import(`${moduleUrl.href}?cacheBust=${Date.now()}`);
414
+ const module = await importConfigModule(`${moduleUrl.href}?cacheBust=${Date.now()}`);
385
415
  const rawConfig = extractConfigExport(module);
386
416
  return {
387
417
  configPath: resolvedPath,
@@ -2029,6 +2059,58 @@ function toTypeKind(code) {
2029
2059
  function escapeSqlLiteral(value) {
2030
2060
  return value.replace(/'/g, "''");
2031
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
+ }
2032
2114
  function buildSchemaArrayLiteral(schemas) {
2033
2115
  const normalized = schemas.length > 0 ? schemas : ["public"];
2034
2116
  const literals = normalized.map((schema) => `'${escapeSqlLiteral(schema)}'`).join(", ");
@@ -2068,8 +2150,10 @@ var PostgresCatalogSnapshotAssembler = class {
2068
2150
  addPrimaryKeyRows(primaryKeyRows) {
2069
2151
  for (const row of primaryKeyRows) {
2070
2152
  const table = this.ensureTable(row.schema_name, row.table_name);
2071
- table.primaryKey = row.columns;
2072
- 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) {
2073
2157
  const column = table.columns[columnName];
2074
2158
  if (column) {
2075
2159
  column.isPrimaryKey = true;
@@ -2081,23 +2165,27 @@ var PostgresCatalogSnapshotAssembler = class {
2081
2165
  for (const row of foreignKeyRows) {
2082
2166
  const sourceTable = this.ensureTable(row.source_schema, row.source_table);
2083
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;
2084
2172
  const sourceRelationKind = row.source_is_unique ? "one-to-one" : "many-to-one";
2085
2173
  this.upsertRelation(sourceTable, relationKey(row.constraint_name, row.target_table), {
2086
2174
  name: row.constraint_name,
2087
2175
  kind: sourceRelationKind,
2088
- sourceColumns: row.source_columns,
2176
+ sourceColumns,
2089
2177
  targetSchema: row.target_schema,
2090
2178
  targetModel: row.target_table,
2091
- targetColumns: row.target_columns
2179
+ targetColumns
2092
2180
  });
2093
2181
  const targetRelationKind = row.source_is_unique ? "one-to-one" : "one-to-many";
2094
2182
  this.upsertRelation(targetTable, relationKey(row.source_table), {
2095
2183
  name: relationKey(row.source_table, row.constraint_name),
2096
2184
  kind: targetRelationKind,
2097
- sourceColumns: row.target_columns,
2185
+ sourceColumns: targetColumns,
2098
2186
  targetSchema: row.source_schema,
2099
2187
  targetModel: row.source_table,
2100
- targetColumns: row.source_columns
2188
+ targetColumns: sourceColumns
2101
2189
  });
2102
2190
  }
2103
2191
  }
@@ -2399,7 +2487,7 @@ async function runSchemaGenerator(options = {}) {
2399
2487
  }
2400
2488
 
2401
2489
  // src/cli/index.ts
2402
- function usage() {
2490
+ function rootUsage() {
2403
2491
  return [
2404
2492
  "athena-js CLI",
2405
2493
  "",
@@ -2408,12 +2496,42 @@ function usage() {
2408
2496
  "",
2409
2497
  "Examples:",
2410
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",
2411
2517
  " athena-js generate --config ./athena.config.ts --dry-run"
2412
2518
  ].join("\n");
2413
2519
  }
2520
+ function usage(topic = "root") {
2521
+ return topic === "generate" ? generateUsage() : rootUsage();
2522
+ }
2414
2523
  function parseCommand(argv) {
2415
- if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
2416
- 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]}".`);
2417
2535
  }
2418
2536
  const [command, ...rest] = argv;
2419
2537
  if (command !== "generate") {
@@ -2423,13 +2541,16 @@ function parseCommand(argv) {
2423
2541
  let dryRun = false;
2424
2542
  for (let index = 0; index < rest.length; index += 1) {
2425
2543
  const token = rest[index];
2544
+ if (token === "--help" || token === "-h") {
2545
+ return { command: "help", topic: "generate" };
2546
+ }
2426
2547
  if (token === "--dry-run") {
2427
2548
  dryRun = true;
2428
2549
  continue;
2429
2550
  }
2430
2551
  if (token === "--config") {
2431
2552
  const nextValue = rest[index + 1];
2432
- if (!nextValue) {
2553
+ if (!nextValue || nextValue.startsWith("-")) {
2433
2554
  throw new Error("Missing value for --config option.");
2434
2555
  }
2435
2556
  configPath = nextValue;
@@ -2444,29 +2565,72 @@ function parseCommand(argv) {
2444
2565
  dryRun
2445
2566
  };
2446
2567
  }
2447
- 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;
2448
2605
  const parsed = parseCommand(argv);
2449
2606
  if (parsed.command === "help") {
2450
- console.log(usage());
2607
+ log(usage(parsed.topic));
2451
2608
  return;
2452
2609
  }
2453
- const result = await runSchemaGenerator({
2454
- configPath: parsed.configPath,
2455
- dryRun: parsed.dryRun
2456
- });
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
+ }
2457
2619
  if (parsed.dryRun) {
2458
- console.log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
2620
+ log(`[dry-run] Generated ${result.files.length} files from ${result.configPath}`);
2459
2621
  for (const file of result.files) {
2460
- console.log(` - ${file.path}`);
2622
+ log(` - ${file.path}`);
2461
2623
  }
2462
2624
  return;
2463
2625
  }
2464
- console.log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
2626
+ log(`Generated ${result.writtenFiles.length} files from ${result.configPath}`);
2465
2627
  for (const filePath of result.writtenFiles) {
2466
- console.log(` - ${filePath}`);
2628
+ log(` - ${filePath}`);
2467
2629
  }
2468
2630
  }
2469
2631
 
2632
+ exports.parseCommand = parseCommand;
2470
2633
  exports.runCLI = runCLI;
2634
+ exports.usage = usage;
2471
2635
  //# sourceMappingURL=index.cjs.map
2472
2636
  //# sourceMappingURL=index.cjs.map