@xylex-group/athena 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # athena-js
2
2
 
3
- current version: `1.4.0`
3
+ current version: `1.4.1`
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 and a React hook for client-side use.
5
5
 
6
6
  ## Install
@@ -153,10 +153,12 @@ Filters accumulate on the builder and are sent together when the query executes.
153
153
 
154
154
  ```ts
155
155
  const { data } = await athena
156
- .from("characters")
157
- .select("id, name")
158
- .eq("active", true) // column = value
159
- .neq("role", "guest") // column != value
156
+ .from("characters")
157
+ .select("id, name")
158
+ .eq("active", true) // column = value
159
+ .eqUuid("session_id", "550e8400-e29b-41d4-a716-446655440000") // explicit UUID cast
160
+ .eqCast("session_id", "550e8400-e29b-41d4-a716-446655440000", "uuid") // explicit cast type
161
+ .neq("role", "guest") // column != value
160
162
  .gt("level", 5) // column > value
161
163
  .gte("score", 100) // column >= value
162
164
  .lt("age", 30) // column < value
@@ -169,8 +171,10 @@ const { data } = await athena
169
171
  .containedBy("tags", ["hero", "villain"]) // array is subset of value
170
172
  .match({ role: "admin", active: true }) // multiple eq filters at once
171
173
  .not("role", "eq", "banned") // NOT col op val
172
- .or("status.eq.active,status.eq.pending"); // OR expression
173
- ```
174
+ .or("status.eq.active,status.eq.pending"); // OR expression
175
+ ```
176
+
177
+ `eq()` now auto-detects UUID-like values on identifier columns (for example `id`, `*_id`, `*uuid*`) and uses a safe typed comparison path so UUID columns no longer require app-side manual `::uuid` / `::text` casts.
174
178
 
175
179
  Canonical style for reads is to call `.select(...)` first, then apply filters:
176
180
 
@@ -8,14 +8,29 @@ type AthenaGatewayEndpointPath = '/gateway/fetch' | '/gateway/insert' | '/gatewa
8
8
  type AthenaCountOption = 'exact' | 'planned' | 'estimated';
9
9
  type AthenaConditionValue = string | number | boolean | null;
10
10
  type AthenaConditionArrayValue = Array<AthenaConditionValue>;
11
+ type AthenaConditionCastType = string;
11
12
  type AthenaConditionOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'is' | 'in' | 'contains' | 'containedBy' | 'not' | 'or';
12
13
  interface AthenaGatewayCondition {
13
14
  column?: string;
14
15
  operator: AthenaConditionOperator;
15
16
  value?: AthenaConditionValue | AthenaConditionArrayValue | string;
17
+ /**
18
+ * Optional explicit cast for `value` (for example `"uuid"`).
19
+ * Older gateways ignore unknown fields; newer gateways may use this hint.
20
+ */
21
+ value_cast?: AthenaConditionCastType;
22
+ /**
23
+ * Optional explicit cast for `column` (for example `"text"`).
24
+ * Used by SDK SQL fallback for typed comparisons.
25
+ */
26
+ column_cast?: AthenaConditionCastType;
16
27
  /** Back-compat shape expected by older gateway implementations */
17
28
  eq_column?: string;
18
29
  eq_value?: AthenaConditionValue | AthenaConditionArrayValue | string;
30
+ /** Optional cast hint aligned with legacy eq_* fields */
31
+ eq_value_cast?: AthenaConditionCastType;
32
+ /** Optional cast hint aligned with legacy eq_* fields */
33
+ eq_column_cast?: AthenaConditionCastType;
19
34
  }
20
35
  type AthenaSortDirection = 'ascending' | 'descending';
21
36
  interface AthenaSortBy {
@@ -205,4 +220,4 @@ declare class AthenaGatewayError extends Error {
205
220
  }
206
221
  declare function isAthenaGatewayError(error: unknown): error is AthenaGatewayError;
207
222
 
208
- export { type AthenaConditionValue as A, type BackendConfig as B, type BackendType as a, type AthenaConditionArrayValue as b, type AthenaConditionOperator as c, type AthenaGatewayCallOptions as d, type AthenaGatewayErrorDetails as e, type AthenaRpcCallOptions as f, AthenaGatewayError as g, type AthenaGatewayErrorCode as h, type AthenaRpcFilter as i, type AthenaRpcFilterOperator as j, type AthenaRpcOrder as k, type AthenaRpcPayload as l, Backend as m, isAthenaGatewayError as n, type AthenaGatewayHookConfig as o, type AthenaGatewayHookResult as p, type AthenaDeletePayload as q, type AthenaFetchPayload as r, type AthenaGatewayResponse as s, type AthenaInsertPayload as t, type AthenaUpdatePayload as u };
223
+ export { type AthenaConditionValue as A, type BackendConfig as B, type BackendType as a, type AthenaConditionCastType as b, type AthenaConditionArrayValue as c, type AthenaConditionOperator as d, type AthenaGatewayCallOptions as e, type AthenaGatewayErrorDetails as f, type AthenaRpcCallOptions as g, AthenaGatewayError as h, type AthenaGatewayErrorCode as i, type AthenaRpcFilter as j, type AthenaRpcFilterOperator as k, type AthenaRpcOrder as l, type AthenaRpcPayload as m, Backend as n, isAthenaGatewayError as o, type AthenaGatewayHookConfig as p, type AthenaGatewayHookResult as q, type AthenaDeletePayload as r, type AthenaFetchPayload as s, type AthenaGatewayResponse as t, type AthenaInsertPayload as u, type AthenaUpdatePayload as v };
@@ -8,14 +8,29 @@ type AthenaGatewayEndpointPath = '/gateway/fetch' | '/gateway/insert' | '/gatewa
8
8
  type AthenaCountOption = 'exact' | 'planned' | 'estimated';
9
9
  type AthenaConditionValue = string | number | boolean | null;
10
10
  type AthenaConditionArrayValue = Array<AthenaConditionValue>;
11
+ type AthenaConditionCastType = string;
11
12
  type AthenaConditionOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'is' | 'in' | 'contains' | 'containedBy' | 'not' | 'or';
12
13
  interface AthenaGatewayCondition {
13
14
  column?: string;
14
15
  operator: AthenaConditionOperator;
15
16
  value?: AthenaConditionValue | AthenaConditionArrayValue | string;
17
+ /**
18
+ * Optional explicit cast for `value` (for example `"uuid"`).
19
+ * Older gateways ignore unknown fields; newer gateways may use this hint.
20
+ */
21
+ value_cast?: AthenaConditionCastType;
22
+ /**
23
+ * Optional explicit cast for `column` (for example `"text"`).
24
+ * Used by SDK SQL fallback for typed comparisons.
25
+ */
26
+ column_cast?: AthenaConditionCastType;
16
27
  /** Back-compat shape expected by older gateway implementations */
17
28
  eq_column?: string;
18
29
  eq_value?: AthenaConditionValue | AthenaConditionArrayValue | string;
30
+ /** Optional cast hint aligned with legacy eq_* fields */
31
+ eq_value_cast?: AthenaConditionCastType;
32
+ /** Optional cast hint aligned with legacy eq_* fields */
33
+ eq_column_cast?: AthenaConditionCastType;
19
34
  }
20
35
  type AthenaSortDirection = 'ascending' | 'descending';
21
36
  interface AthenaSortBy {
@@ -205,4 +220,4 @@ declare class AthenaGatewayError extends Error {
205
220
  }
206
221
  declare function isAthenaGatewayError(error: unknown): error is AthenaGatewayError;
207
222
 
208
- export { type AthenaConditionValue as A, type BackendConfig as B, type BackendType as a, type AthenaConditionArrayValue as b, type AthenaConditionOperator as c, type AthenaGatewayCallOptions as d, type AthenaGatewayErrorDetails as e, type AthenaRpcCallOptions as f, AthenaGatewayError as g, type AthenaGatewayErrorCode as h, type AthenaRpcFilter as i, type AthenaRpcFilterOperator as j, type AthenaRpcOrder as k, type AthenaRpcPayload as l, Backend as m, isAthenaGatewayError as n, type AthenaGatewayHookConfig as o, type AthenaGatewayHookResult as p, type AthenaDeletePayload as q, type AthenaFetchPayload as r, type AthenaGatewayResponse as s, type AthenaInsertPayload as t, type AthenaUpdatePayload as u };
223
+ export { type AthenaConditionValue as A, type BackendConfig as B, type BackendType as a, type AthenaConditionCastType as b, type AthenaConditionArrayValue as c, type AthenaConditionOperator as d, type AthenaGatewayCallOptions as e, type AthenaGatewayErrorDetails as f, type AthenaRpcCallOptions as g, AthenaGatewayError as h, type AthenaGatewayErrorCode as i, type AthenaRpcFilter as j, type AthenaRpcFilterOperator as k, type AthenaRpcOrder as l, type AthenaRpcPayload as m, Backend as n, isAthenaGatewayError as o, type AthenaGatewayHookConfig as p, type AthenaGatewayHookResult as q, type AthenaDeletePayload as r, type AthenaFetchPayload as s, type AthenaGatewayResponse as t, type AthenaInsertPayload as u, type AthenaUpdatePayload as v };
package/dist/index.cjs CHANGED
@@ -368,6 +368,8 @@ function createAthenaGatewayClient(config = {}) {
368
368
 
369
369
  // src/client.ts
370
370
  var DEFAULT_COLUMNS = "*";
371
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
372
+ var SAFE_CAST_PATTERN = /^[a-z_][a-z0-9_]*(?:\[\])?$/i;
371
373
  function formatResult(response) {
372
374
  const result = {
373
375
  data: response.data ?? null,
@@ -448,14 +450,149 @@ function stringifyFilterValue(value) {
448
450
  }
449
451
  return String(value);
450
452
  }
453
+ function isUuidString(value) {
454
+ return UUID_PATTERN.test(value.trim());
455
+ }
456
+ function isUuidIdentifierColumn(column) {
457
+ return column === "id" || /(?:^|_)uuid(?:_|$)/i.test(column) || /_id$/i.test(column);
458
+ }
459
+ function shouldUseUuidTextComparison(column, value) {
460
+ return typeof value === "string" && isUuidString(value) && isUuidIdentifierColumn(column);
461
+ }
462
+ function normalizeCast(cast) {
463
+ const normalized = cast.trim().toLowerCase();
464
+ if (!SAFE_CAST_PATTERN.test(normalized)) {
465
+ throw new Error(`Invalid cast type "${cast}"`);
466
+ }
467
+ return normalized;
468
+ }
469
+ function escapeSqlStringLiteral(value) {
470
+ return value.replace(/'/g, "''");
471
+ }
472
+ function quoteIdentifier(identifier) {
473
+ return `"${identifier.replace(/"/g, '""')}"`;
474
+ }
475
+ function quoteQualifiedIdentifier(identifier) {
476
+ return identifier.split(".").map((segment) => quoteIdentifier(segment)).join(".");
477
+ }
478
+ function toSqlLiteral(value) {
479
+ if (value === null) return "NULL";
480
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
481
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
482
+ return `'${escapeSqlStringLiteral(value)}'`;
483
+ }
484
+ function withCast(expression, cast) {
485
+ if (!cast) return expression;
486
+ return `${expression}::${normalizeCast(cast)}`;
487
+ }
488
+ function buildSelectColumnsClause(columns) {
489
+ if (Array.isArray(columns)) {
490
+ return columns.map((column) => quoteQualifiedIdentifier(column)).join(", ");
491
+ }
492
+ return columns;
493
+ }
494
+ function conditionToSqlClause(condition) {
495
+ if (!condition.column) return null;
496
+ const column = withCast(quoteQualifiedIdentifier(condition.column), condition.column_cast);
497
+ const value = condition.value;
498
+ const sqlOperator = {
499
+ eq: "=",
500
+ neq: "!=",
501
+ gt: ">",
502
+ gte: ">=",
503
+ lt: "<",
504
+ lte: "<=",
505
+ like: "LIKE",
506
+ ilike: "ILIKE"
507
+ };
508
+ switch (condition.operator) {
509
+ case "eq":
510
+ case "neq":
511
+ case "gt":
512
+ case "gte":
513
+ case "lt":
514
+ case "lte":
515
+ case "like":
516
+ case "ilike": {
517
+ if (Array.isArray(value) || value === void 0) return null;
518
+ const rhs = withCast(toSqlLiteral(value), condition.value_cast);
519
+ return `${column} ${sqlOperator[condition.operator]} ${rhs}`;
520
+ }
521
+ case "is": {
522
+ if (value === null) return `${column} IS NULL`;
523
+ if (value === true) return `${column} IS TRUE`;
524
+ if (value === false) return `${column} IS FALSE`;
525
+ return null;
526
+ }
527
+ case "in": {
528
+ if (!Array.isArray(value)) return null;
529
+ if (value.length === 0) return "FALSE";
530
+ const values = value.map((item) => withCast(toSqlLiteral(item), condition.value_cast));
531
+ return `${column} IN (${values.join(", ")})`;
532
+ }
533
+ default:
534
+ return null;
535
+ }
536
+ }
537
+ function buildTypedSelectQuery(input) {
538
+ const whereClauses = [];
539
+ for (const condition of input.conditions) {
540
+ const clause = conditionToSqlClause(condition);
541
+ if (!clause) return null;
542
+ whereClauses.push(clause);
543
+ }
544
+ let limit = input.limit;
545
+ let offset = input.offset;
546
+ if (limit === void 0 && input.pageSize !== void 0) {
547
+ limit = input.pageSize;
548
+ }
549
+ if (offset === void 0 && input.pageSize !== void 0 && input.currentPage !== void 0 && input.currentPage > 0) {
550
+ offset = (input.currentPage - 1) * input.pageSize;
551
+ }
552
+ const sqlParts = [
553
+ `SELECT ${buildSelectColumnsClause(input.columns)} FROM ${quoteQualifiedIdentifier(input.tableName)}`
554
+ ];
555
+ if (whereClauses.length > 0) {
556
+ sqlParts.push(`WHERE ${whereClauses.join(" AND ")}`);
557
+ }
558
+ if (input.order?.field) {
559
+ const direction = input.order.direction === "descending" ? "DESC" : "ASC";
560
+ sqlParts.push(`ORDER BY ${quoteQualifiedIdentifier(input.order.field)} ${direction}`);
561
+ }
562
+ if (limit !== void 0) {
563
+ sqlParts.push(`LIMIT ${Math.max(0, Math.trunc(limit))}`);
564
+ }
565
+ if (offset !== void 0) {
566
+ sqlParts.push(`OFFSET ${Math.max(0, Math.trunc(offset))}`);
567
+ }
568
+ return `${sqlParts.join(" ")};`;
569
+ }
451
570
  function createFilterMethods(state, addCondition, self) {
452
571
  return {
453
572
  eq(column, value) {
454
- addCondition("eq", column, value);
573
+ if (shouldUseUuidTextComparison(column, value)) {
574
+ addCondition("eq", column, value, { columnCast: "text" });
575
+ } else {
576
+ addCondition("eq", column, value);
577
+ }
578
+ return self;
579
+ },
580
+ eqCast(column, value, cast) {
581
+ addCondition("eq", column, value, { valueCast: cast });
582
+ return self;
583
+ },
584
+ eqUuid(column, value) {
585
+ addCondition("eq", column, value, { valueCast: "uuid" });
455
586
  return self;
456
587
  },
457
588
  match(filters) {
458
- Object.entries(filters).forEach(([column, value]) => addCondition("eq", column, value));
589
+ Object.entries(filters).forEach(([column, value]) => {
590
+ if (shouldUseUuidTextComparison(column, value)) {
591
+ addCondition("eq", column, value, { columnCast: "text" });
592
+ } else {
593
+ addCondition("eq", column, value);
594
+ }
595
+ });
459
596
  return self;
460
597
  },
461
598
  range(from, to) {
@@ -679,7 +816,7 @@ function createTableBuilder(tableName, client) {
679
816
  const state = {
680
817
  conditions: []
681
818
  };
682
- const addCondition = (operator, column, value) => {
819
+ const addCondition = (operator, column, value, hints) => {
683
820
  const condition = { operator };
684
821
  if (column) {
685
822
  condition.column = column;
@@ -693,15 +830,47 @@ function createTableBuilder(tableName, client) {
693
830
  condition.eq_value = value;
694
831
  }
695
832
  }
833
+ if (hints?.valueCast) {
834
+ condition.value_cast = hints.valueCast;
835
+ if (operator === "eq") {
836
+ condition.eq_value_cast = hints.valueCast;
837
+ }
838
+ }
839
+ if (hints?.columnCast) {
840
+ condition.column_cast = hints.columnCast;
841
+ if (operator === "eq") {
842
+ condition.eq_column_cast = hints.columnCast;
843
+ }
844
+ }
696
845
  state.conditions.push(condition);
697
846
  };
698
847
  const builder = {};
699
848
  const filterMethods = createFilterMethods(state, addCondition, builder);
700
849
  const runSelect = async (columns = DEFAULT_COLUMNS, options) => {
850
+ const conditions = state.conditions.length ? state.conditions.map((condition) => ({ ...condition })) : void 0;
851
+ const hasTypedEqualityComparison = conditions?.some(
852
+ (condition) => condition.operator === "eq" && (condition.value_cast !== void 0 || condition.column_cast !== void 0)
853
+ ) ?? false;
854
+ if (hasTypedEqualityComparison && !options?.head && !options?.count && conditions) {
855
+ const query = buildTypedSelectQuery({
856
+ tableName,
857
+ columns,
858
+ conditions,
859
+ limit: state.limit,
860
+ offset: state.offset,
861
+ currentPage: state.currentPage,
862
+ pageSize: state.pageSize,
863
+ order: state.order
864
+ });
865
+ if (query) {
866
+ const queryResponse = await client.queryGateway({ query }, options);
867
+ return formatResult(queryResponse);
868
+ }
869
+ }
701
870
  const payload = {
702
871
  table_name: tableName,
703
872
  columns,
704
- conditions: state.conditions.length ? [...state.conditions] : void 0,
873
+ conditions,
705
874
  limit: state.limit,
706
875
  offset: state.offset,
707
876
  current_page: state.currentPage,