@xylex-group/better-auth-athena 1.0.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.
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time to contribute to **better-auth-athena**!
4
+
5
+ ## Development Setup
6
+
7
+ 1. Fork and clone the repository.
8
+ 2. Install dependencies:
9
+ ```bash
10
+ npm install
11
+ ```
12
+ 3. Run the checks:
13
+ ```bash
14
+ npm run typecheck
15
+ npm run test
16
+ npm run build
17
+ ```
18
+
19
+ ## Pull Requests
20
+
21
+ - Keep changes focused and scoped to a single issue.
22
+ - Add or update tests when you change behavior.
23
+ - Ensure `npm run typecheck`, `npm run test`, and `npm run build` succeed before requesting review.
24
+ - Describe the motivation and context in the PR description.
25
+
26
+ ## Reporting Issues
27
+
28
+ If you find a bug, please open an issue with clear reproduction steps, expected behavior, and actual behavior.
@@ -0,0 +1,3 @@
1
+ # Contributors
2
+
3
+ - XYLEX-GROUP
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XYLEX GROUP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # better-auth-athena
2
+
3
+ A Better-Auth database adapter for the `@xylex-group/athena` gateway. It lets Better-Auth read and write data through Athena while keeping column names in `snake_case` as required by the gateway.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install better-auth-athena
9
+ ```
10
+
11
+ This package relies on the following peer dependencies, so ensure they are installed in your project:
12
+
13
+ ```bash
14
+ npm install better-auth @xylex-group/athena
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { betterAuth } from "better-auth";
21
+ import { athenaAdapter } from "better-auth-athena";
22
+
23
+ export const auth = betterAuth({
24
+ database: athenaAdapter({
25
+ url: process.env.ATHENA_URL!,
26
+ apiKey: process.env.ATHENA_API_KEY!,
27
+ client: "my-app",
28
+ }),
29
+ });
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ `athenaAdapter` accepts the following options:
35
+
36
+ | Option | Type | Required | Default | Description |
37
+ | --- | --- | --- | --- | --- |
38
+ | `url` | `string` | ✅ | — | Athena gateway URL. |
39
+ | `apiKey` | `string` | ✅ | — | API key used to authenticate with Athena. |
40
+ | `client` | `string` | ❌ | — | Client name included with gateway requests. |
41
+ | `debugLogs` | `DBAdapterDebugLogOption` | ❌ | `false` | Enables Better-Auth adapter debug logs. |
42
+ | `usePlural` | `boolean` | ❌ | `false` | Treats table names as plural when mapping models. |
43
+
44
+ ## Notes
45
+
46
+ - `findMany` sorting is performed in memory because the Athena SDK does not expose an order-by method on its query builder.
47
+ - The adapter enables JSON, date, boolean, and numeric ID support in Better-Auth.
48
+
49
+ ## Development
50
+
51
+ Node.js 20.19.0 or later is required for the test/build tooling.
52
+
53
+ ```bash
54
+ npm run typecheck
55
+ npm run test
56
+ npm run build
57
+ ```
58
+
59
+ ## CI/CD
60
+
61
+ GitHub Actions runs the typecheck, test, and build steps for every pull request and push. Releases can be published to npm by creating a GitHub release after the CI workflow succeeds.
62
+
63
+ ## Contributing
64
+
65
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup steps and the contribution process.
66
+
67
+ ## Contributors
68
+
69
+ See [CONTRIBUTORS.md](./CONTRIBUTORS.md) for the current list of project contributors.
70
+
71
+ ## License
72
+
73
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ athenaAdapter: () => athenaAdapter
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_adapters = require("better-auth/adapters");
27
+ var import_athena = require("@xylex-group/athena");
28
+ function applyWhere(builder, field, operator, value) {
29
+ switch (operator) {
30
+ case "eq":
31
+ return builder.eq(field, value);
32
+ case "ne":
33
+ return builder.neq(field, value);
34
+ case "gt":
35
+ return builder.gt(field, value);
36
+ case "gte":
37
+ return builder.gte(field, value);
38
+ case "lt":
39
+ return builder.lt(field, value);
40
+ case "lte":
41
+ return builder.lte(field, value);
42
+ case "in":
43
+ return builder.in(field, value);
44
+ case "not_in":
45
+ return builder.not(field, "in", value);
46
+ case "contains":
47
+ return builder.like(field, `%${value}%`);
48
+ case "starts_with":
49
+ return builder.like(field, `${value}%`);
50
+ case "ends_with":
51
+ return builder.like(field, `%${value}`);
52
+ default:
53
+ return builder.eq(field, value);
54
+ }
55
+ }
56
+ var athenaAdapter = (config) => {
57
+ const db = (0, import_athena.createClient)(config.url, config.apiKey, {
58
+ client: config.client
59
+ });
60
+ return (0, import_adapters.createAdapterFactory)({
61
+ config: {
62
+ adapterId: "athena",
63
+ adapterName: "Athena Adapter",
64
+ usePlural: config.usePlural ?? false,
65
+ debugLogs: config.debugLogs ?? false,
66
+ // Athena/Postgres supports all these natively
67
+ supportsJSON: true,
68
+ supportsDates: true,
69
+ supportsBooleans: true,
70
+ supportsNumericIds: true
71
+ },
72
+ adapter: () => {
73
+ return {
74
+ // ------------------------------------------------------------------
75
+ // CREATE
76
+ // ------------------------------------------------------------------
77
+ create: async ({ model, data }) => {
78
+ const { data: result, error } = await db.from(model).insert(data).select();
79
+ if (error) {
80
+ throw new Error(`[AthenaAdapter] create on "${model}" failed: ${error}`);
81
+ }
82
+ const row = Array.isArray(result) ? result[0] : result;
83
+ return row ?? data;
84
+ },
85
+ // ------------------------------------------------------------------
86
+ // UPDATE
87
+ // ------------------------------------------------------------------
88
+ update: async ({ model, where, update }) => {
89
+ let builder = db.from(model).update(update);
90
+ for (const clause of where) {
91
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
92
+ }
93
+ const { data: result, error } = await builder.select();
94
+ if (error) {
95
+ throw new Error(`[AthenaAdapter] update on "${model}" failed: ${error}`);
96
+ }
97
+ const row = Array.isArray(result) ? result[0] : result;
98
+ return row ?? null;
99
+ },
100
+ // ------------------------------------------------------------------
101
+ // UPDATE MANY
102
+ // ------------------------------------------------------------------
103
+ updateMany: async ({ model, where, update }) => {
104
+ let builder = db.from(model).update(update);
105
+ for (const clause of where) {
106
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
107
+ }
108
+ const { data: result, error } = await builder.select();
109
+ if (error) {
110
+ throw new Error(`[AthenaAdapter] updateMany on "${model}" failed: ${error}`);
111
+ }
112
+ return Array.isArray(result) ? result.length : result ? 1 : 0;
113
+ },
114
+ // ------------------------------------------------------------------
115
+ // DELETE
116
+ // ------------------------------------------------------------------
117
+ delete: async ({ model, where }) => {
118
+ let builder = db.from(model);
119
+ for (const clause of where) {
120
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
121
+ }
122
+ const { error } = await builder.delete();
123
+ if (error) {
124
+ throw new Error(`[AthenaAdapter] delete on "${model}" failed: ${error}`);
125
+ }
126
+ },
127
+ // ------------------------------------------------------------------
128
+ // DELETE MANY
129
+ // ------------------------------------------------------------------
130
+ deleteMany: async ({ model, where }) => {
131
+ let builder = db.from(model);
132
+ for (const clause of where) {
133
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
134
+ }
135
+ const { data: result, error } = await builder.delete().select();
136
+ if (error) {
137
+ throw new Error(`[AthenaAdapter] deleteMany on "${model}" failed: ${error}`);
138
+ }
139
+ return Array.isArray(result) ? result.length : result ? 1 : 0;
140
+ },
141
+ // ------------------------------------------------------------------
142
+ // FIND ONE
143
+ // ------------------------------------------------------------------
144
+ findOne: async ({ model, where, select }) => {
145
+ const columns = select && select.length > 0 ? select.join(", ") : void 0;
146
+ let builder = db.from(model).select(columns);
147
+ for (const clause of where) {
148
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
149
+ }
150
+ const { data: result, error } = await builder.limit(1);
151
+ if (error) {
152
+ throw new Error(`[AthenaAdapter] findOne on "${model}" failed: ${error}`);
153
+ }
154
+ const rows = Array.isArray(result) ? result : result ? [result] : [];
155
+ return rows[0] ?? null;
156
+ },
157
+ // ------------------------------------------------------------------
158
+ // FIND MANY
159
+ // ------------------------------------------------------------------
160
+ findMany: async ({ model, where, limit, sortBy, offset, select }) => {
161
+ const columns = select && select.length > 0 ? select.join(", ") : void 0;
162
+ let builder = db.from(model).select(columns);
163
+ if (where) {
164
+ for (const clause of where) {
165
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
166
+ }
167
+ }
168
+ if (limit !== void 0) {
169
+ builder = builder.limit(limit);
170
+ }
171
+ if (offset !== void 0) {
172
+ builder = builder.offset(offset);
173
+ }
174
+ const { data: result, error } = await builder;
175
+ if (error) {
176
+ throw new Error(`[AthenaAdapter] findMany on "${model}" failed: ${error}`);
177
+ }
178
+ const rows = Array.isArray(result) ? result : [];
179
+ if (sortBy) {
180
+ rows.sort((a, b) => {
181
+ const aVal = a[sortBy.field];
182
+ const bVal = b[sortBy.field];
183
+ if (aVal == null && bVal == null) return 0;
184
+ if (aVal == null) return sortBy.direction === "asc" ? -1 : 1;
185
+ if (bVal == null) return sortBy.direction === "asc" ? 1 : -1;
186
+ const cmp = typeof aVal === "string" && typeof bVal === "string" ? aVal.localeCompare(bVal) : aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
187
+ return sortBy.direction === "asc" ? cmp : -cmp;
188
+ });
189
+ }
190
+ return rows;
191
+ },
192
+ // ------------------------------------------------------------------
193
+ // COUNT
194
+ // ------------------------------------------------------------------
195
+ count: async ({ model, where }) => {
196
+ let builder = db.from(model).select();
197
+ if (where) {
198
+ for (const clause of where) {
199
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
200
+ }
201
+ }
202
+ const { data: result, error } = await builder;
203
+ if (error) {
204
+ throw new Error(`[AthenaAdapter] count on "${model}" failed: ${error}`);
205
+ }
206
+ return Array.isArray(result) ? result.length : 0;
207
+ }
208
+ };
209
+ }
210
+ });
211
+ };
212
+ // Annotate the CommonJS export names for ESM import in node:
213
+ 0 && (module.exports = {
214
+ athenaAdapter
215
+ });
216
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\r\n createAdapterFactory,\r\n type AdapterFactory,\r\n type DBAdapterDebugLogOption,\r\n} from \"better-auth/adapters\";\r\nimport type { BetterAuthOptions } from \"better-auth\";\r\nimport { createClient, type SupabaseClient as AthenaClient } from \"@xylex-group/athena\";\r\n\r\n/**\r\n * Configuration options for the Athena adapter.\r\n */\r\nexport interface AthenaAdapterConfig {\r\n /**\r\n * The URL of your Athena gateway.\r\n */\r\n url: string;\r\n /**\r\n * The API key for authenticating with the Athena gateway.\r\n */\r\n apiKey: string;\r\n /**\r\n * The client name sent in requests to the Athena gateway.\r\n */\r\n client?: string;\r\n /**\r\n * Helps you debug issues with the adapter.\r\n */\r\n debugLogs?: DBAdapterDebugLogOption;\r\n /**\r\n * If the table names in the schema are plural.\r\n *\r\n * @default false\r\n */\r\n usePlural?: boolean;\r\n}\r\n\r\ntype AthenaFilterBuilder = {\r\n eq(col: string, val: unknown): AthenaFilterBuilder;\r\n neq(col: string, val: unknown): AthenaFilterBuilder;\r\n gt(col: string, val: unknown): AthenaFilterBuilder;\r\n gte(col: string, val: unknown): AthenaFilterBuilder;\r\n lt(col: string, val: unknown): AthenaFilterBuilder;\r\n lte(col: string, val: unknown): AthenaFilterBuilder;\r\n in(col: string, vals: unknown[]): AthenaFilterBuilder;\r\n not(col: string, op?: string, val?: unknown): AthenaFilterBuilder;\r\n like(col: string, val: string): AthenaFilterBuilder;\r\n};\r\n\r\n/**\r\n * Apply a Better-Auth `CleanedWhere` clause to an Athena filter-chain builder.\r\n */\r\nfunction applyWhere<T extends AthenaFilterBuilder>(\r\n builder: T,\r\n field: string,\r\n operator: string,\r\n value: unknown,\r\n): T {\r\n switch (operator) {\r\n case \"eq\":\r\n return builder.eq(field, value) as T;\r\n case \"ne\":\r\n return builder.neq(field, value) as T;\r\n case \"gt\":\r\n return builder.gt(field, value) as T;\r\n case \"gte\":\r\n return builder.gte(field, value) as T;\r\n case \"lt\":\r\n return builder.lt(field, value) as T;\r\n case \"lte\":\r\n return builder.lte(field, value) as T;\r\n case \"in\":\r\n return builder.in(field, value as unknown[]) as T;\r\n case \"not_in\":\r\n return builder.not(field, \"in\", value) as T;\r\n case \"contains\":\r\n return builder.like(field, `%${value}%`) as T;\r\n case \"starts_with\":\r\n return builder.like(field, `${value}%`) as T;\r\n case \"ends_with\":\r\n return builder.like(field, `%${value}`) as T;\r\n default:\r\n return builder.eq(field, value) as T;\r\n }\r\n}\r\n\r\ntype WhereClause = { field: string; operator: string; value: unknown };\r\n\r\n/**\r\n * Create a Better-Auth database adapter backed by @xylex-group/athena.\r\n *\r\n * Column names are kept in snake_case as required by the Athena gateway.\r\n *\r\n * @example\r\n * ```ts\r\n * import { betterAuth } from \"better-auth\";\r\n * import { athenaAdapter } from \"better-auth-athena\";\r\n *\r\n * export const auth = betterAuth({\r\n * database: athenaAdapter({\r\n * url: process.env.ATHENA_URL!,\r\n * apiKey: process.env.ATHENA_API_KEY!,\r\n * client: \"my-app\",\r\n * }),\r\n * });\r\n * ```\r\n */\r\nexport const athenaAdapter = (config: AthenaAdapterConfig): AdapterFactory<BetterAuthOptions> => {\r\n const db: AthenaClient = createClient(config.url, config.apiKey, {\r\n client: config.client,\r\n });\r\n\r\n return createAdapterFactory({\r\n config: {\r\n adapterId: \"athena\",\r\n adapterName: \"Athena Adapter\",\r\n usePlural: config.usePlural ?? false,\r\n debugLogs: config.debugLogs ?? false,\r\n // Athena/Postgres supports all these natively\r\n supportsJSON: true,\r\n supportsDates: true,\r\n supportsBooleans: true,\r\n supportsNumericIds: true,\r\n },\r\n adapter: () => {\r\n return {\r\n // ------------------------------------------------------------------\r\n // CREATE\r\n // ------------------------------------------------------------------\r\n create: async <T extends Record<string, unknown>>({ model, data }: { model: string; data: T; select?: string[] }) => {\r\n const { data: result, error } = await db\r\n .from(model)\r\n .insert(data)\r\n .select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] create on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n // Athena returns the inserted row(s); take the first one.\r\n const row = Array.isArray(result) ? result[0] : result;\r\n return (row ?? data) as T;\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // UPDATE\r\n // ------------------------------------------------------------------\r\n update: async <T>({ model, where, update }: { model: string; where: WhereClause[]; update: T }) => {\r\n let builder = db.from(model).update(update as Record<string, unknown>);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] update on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n const row = Array.isArray(result) ? result[0] : result;\r\n return (row ?? null) as T | null;\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // UPDATE MANY\r\n // ------------------------------------------------------------------\r\n updateMany: async ({ model, where, update }: { model: string; where: WhereClause[]; update: Record<string, unknown> }) => {\r\n let builder = db.from(model).update(update);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] updateMany on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n return Array.isArray(result) ? result.length : (result ? 1 : 0);\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // DELETE\r\n // ------------------------------------------------------------------\r\n delete: async ({ model, where }: { model: string; where: WhereClause[] }) => {\r\n let builder = db.from(model);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { error } = await builder.delete();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] delete on \"${model}\" failed: ${error}`);\r\n }\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // DELETE MANY\r\n // ------------------------------------------------------------------\r\n deleteMany: async ({ model, where }: { model: string; where: WhereClause[] }) => {\r\n let builder = db.from(model);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.delete().select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] deleteMany on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n return Array.isArray(result) ? result.length : (result ? 1 : 0);\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // FIND ONE\r\n // ------------------------------------------------------------------\r\n findOne: async <T>({ model, where, select }: { model: string; where: WhereClause[]; select?: string[]; join?: unknown }) => {\r\n const columns = select && select.length > 0 ? select.join(\", \") : undefined;\r\n let builder = db.from(model).select(columns);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.limit(1);\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] findOne on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n const rows = Array.isArray(result) ? result : (result ? [result] : []);\r\n return (rows[0] ?? null) as T | null;\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // FIND MANY\r\n // ------------------------------------------------------------------\r\n findMany: async <T>({ model, where, limit, sortBy, offset, select }: {\r\n model: string;\r\n where?: WhereClause[];\r\n limit: number;\r\n select?: string[];\r\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\r\n offset?: number;\r\n join?: unknown;\r\n }) => {\r\n const columns = select && select.length > 0 ? select.join(\", \") : undefined;\r\n let builder = db.from(model).select(columns);\r\n\r\n if (where) {\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n }\r\n\r\n if (limit !== undefined) {\r\n builder = builder.limit(limit);\r\n }\r\n\r\n if (offset !== undefined) {\r\n builder = builder.offset(offset);\r\n }\r\n\r\n const { data: result, error } = await builder;\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] findMany on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n const rows = (Array.isArray(result) ? result : []) as Record<string, unknown>[];\r\n\r\n // The Athena SDK's select chain does not expose a native orderBy/sort\r\n // method, so we sort the returned rows in memory when sortBy is requested.\r\n if (sortBy) {\r\n rows.sort((a, b) => {\r\n const aVal = a[sortBy.field];\r\n const bVal = b[sortBy.field];\r\n if (aVal == null && bVal == null) return 0;\r\n if (aVal == null) return sortBy.direction === \"asc\" ? -1 : 1;\r\n if (bVal == null) return sortBy.direction === \"asc\" ? 1 : -1;\r\n const cmp =\r\n typeof aVal === \"string\" && typeof bVal === \"string\"\r\n ? aVal.localeCompare(bVal)\r\n : aVal < bVal\r\n ? -1\r\n : aVal > bVal\r\n ? 1\r\n : 0;\r\n return sortBy.direction === \"asc\" ? cmp : -cmp;\r\n });\r\n }\r\n\r\n return rows as T[];\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // COUNT\r\n // ------------------------------------------------------------------\r\n count: async ({ model, where }: { model: string; where?: WhereClause[] }) => {\r\n let builder = db.from(model).select();\r\n\r\n if (where) {\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n }\r\n\r\n const { data: result, error } = await builder;\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] count on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n return Array.isArray(result) ? result.length : 0;\r\n },\r\n };\r\n },\r\n });\r\n};\r\n\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAIO;AAEP,oBAAkE;AA6ClE,SAAS,WACP,SACA,OACA,UACA,OACG;AACH,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO,QAAQ,GAAG,OAAO,KAAK;AAAA,IAChC,KAAK;AACH,aAAO,QAAQ,IAAI,OAAO,KAAK;AAAA,IACjC,KAAK;AACH,aAAO,QAAQ,GAAG,OAAO,KAAK;AAAA,IAChC,KAAK;AACH,aAAO,QAAQ,IAAI,OAAO,KAAK;AAAA,IACjC,KAAK;AACH,aAAO,QAAQ,GAAG,OAAO,KAAK;AAAA,IAChC,KAAK;AACH,aAAO,QAAQ,IAAI,OAAO,KAAK;AAAA,IACjC,KAAK;AACH,aAAO,QAAQ,GAAG,OAAO,KAAkB;AAAA,IAC7C,KAAK;AACH,aAAO,QAAQ,IAAI,OAAO,MAAM,KAAK;AAAA,IACvC,KAAK;AACH,aAAO,QAAQ,KAAK,OAAO,IAAI,KAAK,GAAG;AAAA,IACzC,KAAK;AACH,aAAO,QAAQ,KAAK,OAAO,GAAG,KAAK,GAAG;AAAA,IACxC,KAAK;AACH,aAAO,QAAQ,KAAK,OAAO,IAAI,KAAK,EAAE;AAAA,IACxC;AACE,aAAO,QAAQ,GAAG,OAAO,KAAK;AAAA,EAClC;AACF;AAuBO,IAAM,gBAAgB,CAAC,WAAmE;AAC/F,QAAM,SAAmB,4BAAa,OAAO,KAAK,OAAO,QAAQ;AAAA,IAC/D,QAAQ,OAAO;AAAA,EACjB,CAAC;AAED,aAAO,sCAAqB;AAAA,IAC1B,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,aAAa;AAAA,MACb,WAAW,OAAO,aAAa;AAAA,MAC/B,WAAW,OAAO,aAAa;AAAA;AAAA,MAE/B,cAAc;AAAA,MACd,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,oBAAoB;AAAA,IACtB;AAAA,IACA,SAAS,MAAM;AACb,aAAO;AAAA;AAAA;AAAA;AAAA,QAIL,QAAQ,OAA0C,EAAE,OAAO,KAAK,MAAqD;AACnH,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,GACnC,KAAK,KAAK,EACV,OAAO,IAAI,EACX,OAAO;AAEV,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,8BAA8B,KAAK,aAAa,KAAK,EAAE;AAAA,UACzE;AAGA,gBAAM,MAAM,MAAM,QAAQ,MAAM,IAAI,OAAO,CAAC,IAAI;AAChD,iBAAQ,OAAO;AAAA,QACjB;AAAA;AAAA;AAAA;AAAA,QAKA,QAAQ,OAAU,EAAE,OAAO,OAAO,OAAO,MAA0D;AACjG,cAAI,UAAU,GAAG,KAAK,KAAK,EAAE,OAAO,MAAiC;AAErE,qBAAW,UAAU,OAAO;AAC1B,sBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,UAC3E;AAEA,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,QAAQ,OAAO;AAErD,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,8BAA8B,KAAK,aAAa,KAAK,EAAE;AAAA,UACzE;AAEA,gBAAM,MAAM,MAAM,QAAQ,MAAM,IAAI,OAAO,CAAC,IAAI;AAChD,iBAAQ,OAAO;AAAA,QACjB;AAAA;AAAA;AAAA;AAAA,QAKA,YAAY,OAAO,EAAE,OAAO,OAAO,OAAO,MAAgF;AACxH,cAAI,UAAU,GAAG,KAAK,KAAK,EAAE,OAAO,MAAM;AAE1C,qBAAW,UAAU,OAAO;AAC1B,sBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,UAC3E;AAEA,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,QAAQ,OAAO;AAErD,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,kCAAkC,KAAK,aAAa,KAAK,EAAE;AAAA,UAC7E;AAEA,iBAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,SAAU,SAAS,IAAI;AAAA,QAC/D;AAAA;AAAA;AAAA;AAAA,QAKA,QAAQ,OAAO,EAAE,OAAO,MAAM,MAA+C;AAC3E,cAAI,UAAU,GAAG,KAAK,KAAK;AAE3B,qBAAW,UAAU,OAAO;AAC1B,sBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,UAC3E;AAEA,gBAAM,EAAE,MAAM,IAAI,MAAM,QAAQ,OAAO;AAEvC,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,8BAA8B,KAAK,aAAa,KAAK,EAAE;AAAA,UACzE;AAAA,QACF;AAAA;AAAA;AAAA;AAAA,QAKA,YAAY,OAAO,EAAE,OAAO,MAAM,MAA+C;AAC/E,cAAI,UAAU,GAAG,KAAK,KAAK;AAE3B,qBAAW,UAAU,OAAO;AAC1B,sBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,UAC3E;AAEA,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,QAAQ,OAAO,EAAE,OAAO;AAE9D,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,kCAAkC,KAAK,aAAa,KAAK,EAAE;AAAA,UAC7E;AAEA,iBAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,SAAU,SAAS,IAAI;AAAA,QAC/D;AAAA;AAAA;AAAA;AAAA,QAKA,SAAS,OAAU,EAAE,OAAO,OAAO,OAAO,MAAkF;AAC1H,gBAAM,UAAU,UAAU,OAAO,SAAS,IAAI,OAAO,KAAK,IAAI,IAAI;AAClE,cAAI,UAAU,GAAG,KAAK,KAAK,EAAE,OAAO,OAAO;AAE3C,qBAAW,UAAU,OAAO;AAC1B,sBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,UAC3E;AAEA,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM,QAAQ,MAAM,CAAC;AAErD,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,+BAA+B,KAAK,aAAa,KAAK,EAAE;AAAA,UAC1E;AAEA,gBAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAU,SAAS,CAAC,MAAM,IAAI,CAAC;AACpE,iBAAQ,KAAK,CAAC,KAAK;AAAA,QACrB;AAAA;AAAA;AAAA;AAAA,QAKA,UAAU,OAAU,EAAE,OAAO,OAAO,OAAO,QAAQ,QAAQ,OAAO,MAQ5D;AACJ,gBAAM,UAAU,UAAU,OAAO,SAAS,IAAI,OAAO,KAAK,IAAI,IAAI;AAClE,cAAI,UAAU,GAAG,KAAK,KAAK,EAAE,OAAO,OAAO;AAE3C,cAAI,OAAO;AACT,uBAAW,UAAU,OAAO;AAC1B,wBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,YAC3E;AAAA,UACF;AAEA,cAAI,UAAU,QAAW;AACvB,sBAAU,QAAQ,MAAM,KAAK;AAAA,UAC/B;AAEA,cAAI,WAAW,QAAW;AACxB,sBAAU,QAAQ,OAAO,MAAM;AAAA,UACjC;AAEA,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM;AAEtC,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,gCAAgC,KAAK,aAAa,KAAK,EAAE;AAAA,UAC3E;AAEA,gBAAM,OAAQ,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC;AAIhD,cAAI,QAAQ;AACV,iBAAK,KAAK,CAAC,GAAG,MAAM;AAClB,oBAAM,OAAO,EAAE,OAAO,KAAK;AAC3B,oBAAM,OAAO,EAAE,OAAO,KAAK;AAC3B,kBAAI,QAAQ,QAAQ,QAAQ,KAAM,QAAO;AACzC,kBAAI,QAAQ,KAAM,QAAO,OAAO,cAAc,QAAQ,KAAK;AAC3D,kBAAI,QAAQ,KAAM,QAAO,OAAO,cAAc,QAAQ,IAAI;AAC1D,oBAAM,MACJ,OAAO,SAAS,YAAY,OAAO,SAAS,WACxC,KAAK,cAAc,IAAI,IACvB,OAAO,OACP,KACA,OAAO,OACP,IACA;AACN,qBAAO,OAAO,cAAc,QAAQ,MAAM,CAAC;AAAA,YAC7C,CAAC;AAAA,UACH;AAEA,iBAAO;AAAA,QACT;AAAA;AAAA;AAAA;AAAA,QAKA,OAAO,OAAO,EAAE,OAAO,MAAM,MAAgD;AAC3E,cAAI,UAAU,GAAG,KAAK,KAAK,EAAE,OAAO;AAEpC,cAAI,OAAO;AACT,uBAAW,UAAU,OAAO;AAC1B,wBAAU,WAAW,SAAS,OAAO,OAAO,OAAO,UAAU,OAAO,KAAK;AAAA,YAC3E;AAAA,UACF;AAEA,gBAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,MAAM;AAEtC,cAAI,OAAO;AACT,kBAAM,IAAI,MAAM,6BAA6B,KAAK,aAAa,KAAK,EAAE;AAAA,UACxE;AAEA,iBAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,SAAS;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,52 @@
1
+ import { DBAdapterDebugLogOption, AdapterFactory } from 'better-auth/adapters';
2
+ import { BetterAuthOptions } from 'better-auth';
3
+
4
+ /**
5
+ * Configuration options for the Athena adapter.
6
+ */
7
+ interface AthenaAdapterConfig {
8
+ /**
9
+ * The URL of your Athena gateway.
10
+ */
11
+ url: string;
12
+ /**
13
+ * The API key for authenticating with the Athena gateway.
14
+ */
15
+ apiKey: string;
16
+ /**
17
+ * The client name sent in requests to the Athena gateway.
18
+ */
19
+ client?: string;
20
+ /**
21
+ * Helps you debug issues with the adapter.
22
+ */
23
+ debugLogs?: DBAdapterDebugLogOption;
24
+ /**
25
+ * If the table names in the schema are plural.
26
+ *
27
+ * @default false
28
+ */
29
+ usePlural?: boolean;
30
+ }
31
+ /**
32
+ * Create a Better-Auth database adapter backed by @xylex-group/athena.
33
+ *
34
+ * Column names are kept in snake_case as required by the Athena gateway.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { betterAuth } from "better-auth";
39
+ * import { athenaAdapter } from "better-auth-athena";
40
+ *
41
+ * export const auth = betterAuth({
42
+ * database: athenaAdapter({
43
+ * url: process.env.ATHENA_URL!,
44
+ * apiKey: process.env.ATHENA_API_KEY!,
45
+ * client: "my-app",
46
+ * }),
47
+ * });
48
+ * ```
49
+ */
50
+ declare const athenaAdapter: (config: AthenaAdapterConfig) => AdapterFactory<BetterAuthOptions>;
51
+
52
+ export { type AthenaAdapterConfig, athenaAdapter };
@@ -0,0 +1,52 @@
1
+ import { DBAdapterDebugLogOption, AdapterFactory } from 'better-auth/adapters';
2
+ import { BetterAuthOptions } from 'better-auth';
3
+
4
+ /**
5
+ * Configuration options for the Athena adapter.
6
+ */
7
+ interface AthenaAdapterConfig {
8
+ /**
9
+ * The URL of your Athena gateway.
10
+ */
11
+ url: string;
12
+ /**
13
+ * The API key for authenticating with the Athena gateway.
14
+ */
15
+ apiKey: string;
16
+ /**
17
+ * The client name sent in requests to the Athena gateway.
18
+ */
19
+ client?: string;
20
+ /**
21
+ * Helps you debug issues with the adapter.
22
+ */
23
+ debugLogs?: DBAdapterDebugLogOption;
24
+ /**
25
+ * If the table names in the schema are plural.
26
+ *
27
+ * @default false
28
+ */
29
+ usePlural?: boolean;
30
+ }
31
+ /**
32
+ * Create a Better-Auth database adapter backed by @xylex-group/athena.
33
+ *
34
+ * Column names are kept in snake_case as required by the Athena gateway.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { betterAuth } from "better-auth";
39
+ * import { athenaAdapter } from "better-auth-athena";
40
+ *
41
+ * export const auth = betterAuth({
42
+ * database: athenaAdapter({
43
+ * url: process.env.ATHENA_URL!,
44
+ * apiKey: process.env.ATHENA_API_KEY!,
45
+ * client: "my-app",
46
+ * }),
47
+ * });
48
+ * ```
49
+ */
50
+ declare const athenaAdapter: (config: AthenaAdapterConfig) => AdapterFactory<BetterAuthOptions>;
51
+
52
+ export { type AthenaAdapterConfig, athenaAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,193 @@
1
+ // src/index.ts
2
+ import {
3
+ createAdapterFactory
4
+ } from "better-auth/adapters";
5
+ import { createClient } from "@xylex-group/athena";
6
+ function applyWhere(builder, field, operator, value) {
7
+ switch (operator) {
8
+ case "eq":
9
+ return builder.eq(field, value);
10
+ case "ne":
11
+ return builder.neq(field, value);
12
+ case "gt":
13
+ return builder.gt(field, value);
14
+ case "gte":
15
+ return builder.gte(field, value);
16
+ case "lt":
17
+ return builder.lt(field, value);
18
+ case "lte":
19
+ return builder.lte(field, value);
20
+ case "in":
21
+ return builder.in(field, value);
22
+ case "not_in":
23
+ return builder.not(field, "in", value);
24
+ case "contains":
25
+ return builder.like(field, `%${value}%`);
26
+ case "starts_with":
27
+ return builder.like(field, `${value}%`);
28
+ case "ends_with":
29
+ return builder.like(field, `%${value}`);
30
+ default:
31
+ return builder.eq(field, value);
32
+ }
33
+ }
34
+ var athenaAdapter = (config) => {
35
+ const db = createClient(config.url, config.apiKey, {
36
+ client: config.client
37
+ });
38
+ return createAdapterFactory({
39
+ config: {
40
+ adapterId: "athena",
41
+ adapterName: "Athena Adapter",
42
+ usePlural: config.usePlural ?? false,
43
+ debugLogs: config.debugLogs ?? false,
44
+ // Athena/Postgres supports all these natively
45
+ supportsJSON: true,
46
+ supportsDates: true,
47
+ supportsBooleans: true,
48
+ supportsNumericIds: true
49
+ },
50
+ adapter: () => {
51
+ return {
52
+ // ------------------------------------------------------------------
53
+ // CREATE
54
+ // ------------------------------------------------------------------
55
+ create: async ({ model, data }) => {
56
+ const { data: result, error } = await db.from(model).insert(data).select();
57
+ if (error) {
58
+ throw new Error(`[AthenaAdapter] create on "${model}" failed: ${error}`);
59
+ }
60
+ const row = Array.isArray(result) ? result[0] : result;
61
+ return row ?? data;
62
+ },
63
+ // ------------------------------------------------------------------
64
+ // UPDATE
65
+ // ------------------------------------------------------------------
66
+ update: async ({ model, where, update }) => {
67
+ let builder = db.from(model).update(update);
68
+ for (const clause of where) {
69
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
70
+ }
71
+ const { data: result, error } = await builder.select();
72
+ if (error) {
73
+ throw new Error(`[AthenaAdapter] update on "${model}" failed: ${error}`);
74
+ }
75
+ const row = Array.isArray(result) ? result[0] : result;
76
+ return row ?? null;
77
+ },
78
+ // ------------------------------------------------------------------
79
+ // UPDATE MANY
80
+ // ------------------------------------------------------------------
81
+ updateMany: async ({ model, where, update }) => {
82
+ let builder = db.from(model).update(update);
83
+ for (const clause of where) {
84
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
85
+ }
86
+ const { data: result, error } = await builder.select();
87
+ if (error) {
88
+ throw new Error(`[AthenaAdapter] updateMany on "${model}" failed: ${error}`);
89
+ }
90
+ return Array.isArray(result) ? result.length : result ? 1 : 0;
91
+ },
92
+ // ------------------------------------------------------------------
93
+ // DELETE
94
+ // ------------------------------------------------------------------
95
+ delete: async ({ model, where }) => {
96
+ let builder = db.from(model);
97
+ for (const clause of where) {
98
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
99
+ }
100
+ const { error } = await builder.delete();
101
+ if (error) {
102
+ throw new Error(`[AthenaAdapter] delete on "${model}" failed: ${error}`);
103
+ }
104
+ },
105
+ // ------------------------------------------------------------------
106
+ // DELETE MANY
107
+ // ------------------------------------------------------------------
108
+ deleteMany: async ({ model, where }) => {
109
+ let builder = db.from(model);
110
+ for (const clause of where) {
111
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
112
+ }
113
+ const { data: result, error } = await builder.delete().select();
114
+ if (error) {
115
+ throw new Error(`[AthenaAdapter] deleteMany on "${model}" failed: ${error}`);
116
+ }
117
+ return Array.isArray(result) ? result.length : result ? 1 : 0;
118
+ },
119
+ // ------------------------------------------------------------------
120
+ // FIND ONE
121
+ // ------------------------------------------------------------------
122
+ findOne: async ({ model, where, select }) => {
123
+ const columns = select && select.length > 0 ? select.join(", ") : void 0;
124
+ let builder = db.from(model).select(columns);
125
+ for (const clause of where) {
126
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
127
+ }
128
+ const { data: result, error } = await builder.limit(1);
129
+ if (error) {
130
+ throw new Error(`[AthenaAdapter] findOne on "${model}" failed: ${error}`);
131
+ }
132
+ const rows = Array.isArray(result) ? result : result ? [result] : [];
133
+ return rows[0] ?? null;
134
+ },
135
+ // ------------------------------------------------------------------
136
+ // FIND MANY
137
+ // ------------------------------------------------------------------
138
+ findMany: async ({ model, where, limit, sortBy, offset, select }) => {
139
+ const columns = select && select.length > 0 ? select.join(", ") : void 0;
140
+ let builder = db.from(model).select(columns);
141
+ if (where) {
142
+ for (const clause of where) {
143
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
144
+ }
145
+ }
146
+ if (limit !== void 0) {
147
+ builder = builder.limit(limit);
148
+ }
149
+ if (offset !== void 0) {
150
+ builder = builder.offset(offset);
151
+ }
152
+ const { data: result, error } = await builder;
153
+ if (error) {
154
+ throw new Error(`[AthenaAdapter] findMany on "${model}" failed: ${error}`);
155
+ }
156
+ const rows = Array.isArray(result) ? result : [];
157
+ if (sortBy) {
158
+ rows.sort((a, b) => {
159
+ const aVal = a[sortBy.field];
160
+ const bVal = b[sortBy.field];
161
+ if (aVal == null && bVal == null) return 0;
162
+ if (aVal == null) return sortBy.direction === "asc" ? -1 : 1;
163
+ if (bVal == null) return sortBy.direction === "asc" ? 1 : -1;
164
+ const cmp = typeof aVal === "string" && typeof bVal === "string" ? aVal.localeCompare(bVal) : aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
165
+ return sortBy.direction === "asc" ? cmp : -cmp;
166
+ });
167
+ }
168
+ return rows;
169
+ },
170
+ // ------------------------------------------------------------------
171
+ // COUNT
172
+ // ------------------------------------------------------------------
173
+ count: async ({ model, where }) => {
174
+ let builder = db.from(model).select();
175
+ if (where) {
176
+ for (const clause of where) {
177
+ builder = applyWhere(builder, clause.field, clause.operator, clause.value);
178
+ }
179
+ }
180
+ const { data: result, error } = await builder;
181
+ if (error) {
182
+ throw new Error(`[AthenaAdapter] count on "${model}" failed: ${error}`);
183
+ }
184
+ return Array.isArray(result) ? result.length : 0;
185
+ }
186
+ };
187
+ }
188
+ });
189
+ };
190
+ export {
191
+ athenaAdapter
192
+ };
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\r\n createAdapterFactory,\r\n type AdapterFactory,\r\n type DBAdapterDebugLogOption,\r\n} from \"better-auth/adapters\";\r\nimport type { BetterAuthOptions } from \"better-auth\";\r\nimport { createClient, type SupabaseClient as AthenaClient } from \"@xylex-group/athena\";\r\n\r\n/**\r\n * Configuration options for the Athena adapter.\r\n */\r\nexport interface AthenaAdapterConfig {\r\n /**\r\n * The URL of your Athena gateway.\r\n */\r\n url: string;\r\n /**\r\n * The API key for authenticating with the Athena gateway.\r\n */\r\n apiKey: string;\r\n /**\r\n * The client name sent in requests to the Athena gateway.\r\n */\r\n client?: string;\r\n /**\r\n * Helps you debug issues with the adapter.\r\n */\r\n debugLogs?: DBAdapterDebugLogOption;\r\n /**\r\n * If the table names in the schema are plural.\r\n *\r\n * @default false\r\n */\r\n usePlural?: boolean;\r\n}\r\n\r\ntype AthenaFilterBuilder = {\r\n eq(col: string, val: unknown): AthenaFilterBuilder;\r\n neq(col: string, val: unknown): AthenaFilterBuilder;\r\n gt(col: string, val: unknown): AthenaFilterBuilder;\r\n gte(col: string, val: unknown): AthenaFilterBuilder;\r\n lt(col: string, val: unknown): AthenaFilterBuilder;\r\n lte(col: string, val: unknown): AthenaFilterBuilder;\r\n in(col: string, vals: unknown[]): AthenaFilterBuilder;\r\n not(col: string, op?: string, val?: unknown): AthenaFilterBuilder;\r\n like(col: string, val: string): AthenaFilterBuilder;\r\n};\r\n\r\n/**\r\n * Apply a Better-Auth `CleanedWhere` clause to an Athena filter-chain builder.\r\n */\r\nfunction applyWhere<T extends AthenaFilterBuilder>(\r\n builder: T,\r\n field: string,\r\n operator: string,\r\n value: unknown,\r\n): T {\r\n switch (operator) {\r\n case \"eq\":\r\n return builder.eq(field, value) as T;\r\n case \"ne\":\r\n return builder.neq(field, value) as T;\r\n case \"gt\":\r\n return builder.gt(field, value) as T;\r\n case \"gte\":\r\n return builder.gte(field, value) as T;\r\n case \"lt\":\r\n return builder.lt(field, value) as T;\r\n case \"lte\":\r\n return builder.lte(field, value) as T;\r\n case \"in\":\r\n return builder.in(field, value as unknown[]) as T;\r\n case \"not_in\":\r\n return builder.not(field, \"in\", value) as T;\r\n case \"contains\":\r\n return builder.like(field, `%${value}%`) as T;\r\n case \"starts_with\":\r\n return builder.like(field, `${value}%`) as T;\r\n case \"ends_with\":\r\n return builder.like(field, `%${value}`) as T;\r\n default:\r\n return builder.eq(field, value) as T;\r\n }\r\n}\r\n\r\ntype WhereClause = { field: string; operator: string; value: unknown };\r\n\r\n/**\r\n * Create a Better-Auth database adapter backed by @xylex-group/athena.\r\n *\r\n * Column names are kept in snake_case as required by the Athena gateway.\r\n *\r\n * @example\r\n * ```ts\r\n * import { betterAuth } from \"better-auth\";\r\n * import { athenaAdapter } from \"better-auth-athena\";\r\n *\r\n * export const auth = betterAuth({\r\n * database: athenaAdapter({\r\n * url: process.env.ATHENA_URL!,\r\n * apiKey: process.env.ATHENA_API_KEY!,\r\n * client: \"my-app\",\r\n * }),\r\n * });\r\n * ```\r\n */\r\nexport const athenaAdapter = (config: AthenaAdapterConfig): AdapterFactory<BetterAuthOptions> => {\r\n const db: AthenaClient = createClient(config.url, config.apiKey, {\r\n client: config.client,\r\n });\r\n\r\n return createAdapterFactory({\r\n config: {\r\n adapterId: \"athena\",\r\n adapterName: \"Athena Adapter\",\r\n usePlural: config.usePlural ?? false,\r\n debugLogs: config.debugLogs ?? false,\r\n // Athena/Postgres supports all these natively\r\n supportsJSON: true,\r\n supportsDates: true,\r\n supportsBooleans: true,\r\n supportsNumericIds: true,\r\n },\r\n adapter: () => {\r\n return {\r\n // ------------------------------------------------------------------\r\n // CREATE\r\n // ------------------------------------------------------------------\r\n create: async <T extends Record<string, unknown>>({ model, data }: { model: string; data: T; select?: string[] }) => {\r\n const { data: result, error } = await db\r\n .from(model)\r\n .insert(data)\r\n .select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] create on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n // Athena returns the inserted row(s); take the first one.\r\n const row = Array.isArray(result) ? result[0] : result;\r\n return (row ?? data) as T;\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // UPDATE\r\n // ------------------------------------------------------------------\r\n update: async <T>({ model, where, update }: { model: string; where: WhereClause[]; update: T }) => {\r\n let builder = db.from(model).update(update as Record<string, unknown>);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] update on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n const row = Array.isArray(result) ? result[0] : result;\r\n return (row ?? null) as T | null;\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // UPDATE MANY\r\n // ------------------------------------------------------------------\r\n updateMany: async ({ model, where, update }: { model: string; where: WhereClause[]; update: Record<string, unknown> }) => {\r\n let builder = db.from(model).update(update);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] updateMany on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n return Array.isArray(result) ? result.length : (result ? 1 : 0);\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // DELETE\r\n // ------------------------------------------------------------------\r\n delete: async ({ model, where }: { model: string; where: WhereClause[] }) => {\r\n let builder = db.from(model);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { error } = await builder.delete();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] delete on \"${model}\" failed: ${error}`);\r\n }\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // DELETE MANY\r\n // ------------------------------------------------------------------\r\n deleteMany: async ({ model, where }: { model: string; where: WhereClause[] }) => {\r\n let builder = db.from(model);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.delete().select();\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] deleteMany on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n return Array.isArray(result) ? result.length : (result ? 1 : 0);\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // FIND ONE\r\n // ------------------------------------------------------------------\r\n findOne: async <T>({ model, where, select }: { model: string; where: WhereClause[]; select?: string[]; join?: unknown }) => {\r\n const columns = select && select.length > 0 ? select.join(\", \") : undefined;\r\n let builder = db.from(model).select(columns);\r\n\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n\r\n const { data: result, error } = await builder.limit(1);\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] findOne on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n const rows = Array.isArray(result) ? result : (result ? [result] : []);\r\n return (rows[0] ?? null) as T | null;\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // FIND MANY\r\n // ------------------------------------------------------------------\r\n findMany: async <T>({ model, where, limit, sortBy, offset, select }: {\r\n model: string;\r\n where?: WhereClause[];\r\n limit: number;\r\n select?: string[];\r\n sortBy?: { field: string; direction: \"asc\" | \"desc\" };\r\n offset?: number;\r\n join?: unknown;\r\n }) => {\r\n const columns = select && select.length > 0 ? select.join(\", \") : undefined;\r\n let builder = db.from(model).select(columns);\r\n\r\n if (where) {\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n }\r\n\r\n if (limit !== undefined) {\r\n builder = builder.limit(limit);\r\n }\r\n\r\n if (offset !== undefined) {\r\n builder = builder.offset(offset);\r\n }\r\n\r\n const { data: result, error } = await builder;\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] findMany on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n const rows = (Array.isArray(result) ? result : []) as Record<string, unknown>[];\r\n\r\n // The Athena SDK's select chain does not expose a native orderBy/sort\r\n // method, so we sort the returned rows in memory when sortBy is requested.\r\n if (sortBy) {\r\n rows.sort((a, b) => {\r\n const aVal = a[sortBy.field];\r\n const bVal = b[sortBy.field];\r\n if (aVal == null && bVal == null) return 0;\r\n if (aVal == null) return sortBy.direction === \"asc\" ? -1 : 1;\r\n if (bVal == null) return sortBy.direction === \"asc\" ? 1 : -1;\r\n const cmp =\r\n typeof aVal === \"string\" && typeof bVal === \"string\"\r\n ? aVal.localeCompare(bVal)\r\n : aVal < bVal\r\n ? -1\r\n : aVal > bVal\r\n ? 1\r\n : 0;\r\n return sortBy.direction === \"asc\" ? cmp : -cmp;\r\n });\r\n }\r\n\r\n return rows as T[];\r\n },\r\n\r\n // ------------------------------------------------------------------\r\n // COUNT\r\n // ------------------------------------------------------------------\r\n count: async ({ model, where }: { model: string; where?: WhereClause[] }) => {\r\n let builder = db.from(model).select();\r\n\r\n if (where) {\r\n for (const clause of where) {\r\n builder = applyWhere(builder, clause.field, clause.operator, clause.value);\r\n }\r\n }\r\n\r\n const { data: result, error } = await builder;\r\n\r\n if (error) {\r\n throw new Error(`[AthenaAdapter] count on \"${model}\" failed: ${error}`);\r\n }\r\n\r\n return Array.isArray(result) ? result.length : 0;\r\n },\r\n };\r\n },\r\n });\r\n};\r\n\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@xylex-group/better-auth-athena",
3
+ "version": "1.0.0",
4
+ "description": "A better-Auth database adapter for @xylex-group/athena",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE",
19
+ "CONTRIBUTING.md",
20
+ "CONTRIBUTORS.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "prepublishOnly": "npm run build && npm run typecheck && npm run test"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/xylex-group/better-auth-athena.git"
33
+ },
34
+ "keywords": [
35
+ "better-auth",
36
+ "athena",
37
+ "database",
38
+ "adapter"
39
+ ],
40
+ "author": "XYLEX GROUP",
41
+ "contributors": [
42
+ "floris-xlx"
43
+ ],
44
+ "license": "MIT",
45
+ "type": "module",
46
+ "engines": {
47
+ "node": ">=20.19.0"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/xylex-group/better-auth-athena/issues"
51
+ },
52
+ "homepage": "https://github.com/xylex-group/better-auth-athena#readme",
53
+ "peerDependencies": {
54
+ "@xylex-group/athena": "^1.1.1",
55
+ "better-auth": "^1.5.5"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.5.0",
59
+ "@xylex-group/athena": "^1.1.1",
60
+ "better-auth": "^1.5.5",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^4.1.0"
64
+ }
65
+ }