@xylex-group/better-auth-athena 1.0.0 → 1.0.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/dist/index.js CHANGED
@@ -2,39 +2,202 @@
2
2
  import {
3
3
  createAdapterFactory
4
4
  } from "better-auth/adapters";
5
- import { createClient } from "@xylex-group/athena";
6
- function applyWhere(builder, field, operator, value) {
5
+ import {
6
+ createClient
7
+ } from "@xylex-group/athena";
8
+
9
+ // src/config.ts
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import YAML from "yaml";
13
+ var defaultAthenaGlobalConfig = {
14
+ athena: {
15
+ url: "http://localhost:3000",
16
+ apiKey: "",
17
+ client: "better-auth"
18
+ }
19
+ };
20
+ var DEFAULT_CONFIG_FILENAME = "config.yaml";
21
+ function resolveConfigPath(configPath) {
22
+ if (configPath) return path.resolve(configPath);
23
+ return path.resolve(process.cwd(), DEFAULT_CONFIG_FILENAME);
24
+ }
25
+ var cached = null;
26
+ var cachedConfigPath = null;
27
+ var version = 0;
28
+ var watcher = null;
29
+ function isObject(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+ function deepMerge(base, partial) {
33
+ if (!isObject(partial)) return base;
34
+ const out = { ...base };
35
+ for (const [k, v] of Object.entries(partial)) {
36
+ if (v && isObject(v) && isObject(out[k])) {
37
+ out[k] = deepMerge(out[k], v);
38
+ } else {
39
+ out[k] = v;
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ function ensureConfigFile(configPath) {
45
+ const dir = path.dirname(configPath);
46
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
47
+ if (!fs.existsSync(configPath)) {
48
+ const yaml = YAML.stringify(defaultAthenaGlobalConfig);
49
+ fs.writeFileSync(configPath, yaml, "utf-8");
50
+ }
51
+ }
52
+ function readConfigFromDisk(configPath) {
53
+ ensureConfigFile(configPath);
54
+ const raw = fs.readFileSync(configPath, "utf-8");
55
+ const parsed = YAML.parse(raw);
56
+ return deepMerge(defaultAthenaGlobalConfig, parsed);
57
+ }
58
+ function startWatcher(configPath) {
59
+ if (cachedConfigPath !== null && cachedConfigPath !== configPath && watcher) {
60
+ try {
61
+ watcher.close();
62
+ } catch {
63
+ }
64
+ watcher = null;
65
+ }
66
+ if (watcher || cachedConfigPath === configPath) return;
67
+ try {
68
+ watcher = fs.watch(configPath, { persistent: false }, (event) => {
69
+ if (event !== "change" && event !== "rename") return;
70
+ try {
71
+ cached = readConfigFromDisk(configPath);
72
+ version += 1;
73
+ } catch {
74
+ }
75
+ });
76
+ cachedConfigPath = configPath;
77
+ } catch {
78
+ }
79
+ }
80
+ function getAthenaGlobalConfig(options) {
81
+ const configPath = resolveConfigPath(options?.configPath);
82
+ const shouldWatch = options?.watch ?? true;
83
+ if (!cached || cachedConfigPath !== configPath) {
84
+ cached = readConfigFromDisk(configPath);
85
+ cachedConfigPath = configPath;
86
+ version += 1;
87
+ }
88
+ if (shouldWatch) startWatcher(configPath);
89
+ return { config: cached, version };
90
+ }
91
+
92
+ // src/index.ts
93
+ function toSnakeCase(key) {
94
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/__/g, "_").toLowerCase();
95
+ }
96
+ function toCamelCase(key) {
97
+ return key.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase());
98
+ }
99
+ function hasUppercase(key) {
100
+ return /[A-Z]/.test(key);
101
+ }
102
+ function mapKeys(obj, mapKey) {
103
+ const out = {};
104
+ for (const [k, v] of Object.entries(obj)) out[mapKey(k)] = v;
105
+ return out;
106
+ }
107
+ function mapRowToBetterAuth(row) {
108
+ if (!row || typeof row !== "object") return row;
109
+ if (Array.isArray(row)) return row.map(mapRowToBetterAuth);
110
+ return mapKeys(row, toCamelCase);
111
+ }
112
+ function isLikelyIsoDateString(value) {
113
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) return false;
114
+ const ms = Date.parse(value);
115
+ return Number.isFinite(ms);
116
+ }
117
+ function isTimestampKey(key) {
118
+ return key.endsWith("At") || key.endsWith("_at") || key === "expires";
119
+ }
120
+ function coerceDateFields(data) {
121
+ const out = { ...data };
122
+ for (const [key, val] of Object.entries(out)) {
123
+ if (val == null) continue;
124
+ if (typeof val === "string" && isTimestampKey(key) && isLikelyIsoDateString(val)) {
125
+ out[key] = new Date(val);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ function toDbRecord(data) {
131
+ const withDbKeys = mapKeys(
132
+ data,
133
+ (k) => hasUppercase(k) ? toSnakeCase(k) : k
134
+ );
135
+ return coerceDateFields(withDbKeys);
136
+ }
137
+ function applyWhere(builder, field, operator, value, columnMapper = (col) => hasUppercase(col) ? toSnakeCase(col) : col) {
138
+ const dbField = columnMapper(field);
7
139
  switch (operator) {
8
140
  case "eq":
9
- return builder.eq(field, value);
141
+ return builder.eq(dbField, value);
10
142
  case "ne":
11
- return builder.neq(field, value);
143
+ return builder.neq(dbField, value);
12
144
  case "gt":
13
- return builder.gt(field, value);
145
+ return builder.gt(dbField, value);
14
146
  case "gte":
15
- return builder.gte(field, value);
147
+ return builder.gte(dbField, value);
16
148
  case "lt":
17
- return builder.lt(field, value);
149
+ return builder.lt(dbField, value);
18
150
  case "lte":
19
- return builder.lte(field, value);
151
+ return builder.lte(dbField, value);
20
152
  case "in":
21
- return builder.in(field, value);
153
+ return builder.in(dbField, value);
22
154
  case "not_in":
23
- return builder.not(field, "in", value);
155
+ return builder.not(dbField, "in", value);
24
156
  case "contains":
25
- return builder.like(field, `%${value}%`);
157
+ return builder.like(dbField, `%${value}%`);
26
158
  case "starts_with":
27
- return builder.like(field, `${value}%`);
159
+ return builder.like(dbField, `${value}%`);
28
160
  case "ends_with":
29
- return builder.like(field, `%${value}`);
161
+ return builder.like(dbField, `%${value}`);
30
162
  default:
31
- return builder.eq(field, value);
163
+ return builder.eq(dbField, value);
32
164
  }
33
165
  }
166
+ function isMissingColumnError(error) {
167
+ const msg = String(error ?? "");
168
+ return msg.includes("specified column does not exist") || msg.includes("column does not exist");
169
+ }
34
170
  var athenaAdapter = (config) => {
35
- const db = createClient(config.url, config.apiKey, {
36
- client: config.client
37
- });
171
+ let dbClient = null;
172
+ let lastDbConfigVersion = -1;
173
+ const shouldUseFixedConfig = typeof config.url === "string" && config.url.length > 0 && typeof config.apiKey === "string" && config.apiKey.length > 0;
174
+ function ensureDbClient() {
175
+ if (shouldUseFixedConfig) {
176
+ if (!dbClient) {
177
+ dbClient = createClient(config.url, config.apiKey, {
178
+ client: config.client
179
+ });
180
+ }
181
+ return dbClient;
182
+ }
183
+ const { config: globalConfig, version: version2 } = getAthenaGlobalConfig({
184
+ configPath: config.configPath,
185
+ watch: config.watchConfig ?? true
186
+ });
187
+ const url = config.url ?? globalConfig.athena.url;
188
+ const apiKey = config.apiKey ?? globalConfig.athena.apiKey;
189
+ const client = config.client ?? globalConfig.athena.client;
190
+ if (!url || !apiKey) {
191
+ throw new Error(
192
+ `[AthenaAdapter] Missing Athena connection details. Set both 'athena.url' and 'athena.apiKey' in config.yaml (or pass 'url'/'apiKey' to athenaAdapter).`
193
+ );
194
+ }
195
+ if (!dbClient || version2 !== lastDbConfigVersion) {
196
+ dbClient = createClient(url, apiKey, { client });
197
+ lastDbConfigVersion = version2;
198
+ }
199
+ return dbClient;
200
+ }
38
201
  return createAdapterFactory({
39
202
  config: {
40
203
  adapterId: "athena",
@@ -45,143 +208,297 @@ var athenaAdapter = (config) => {
45
208
  supportsJSON: true,
46
209
  supportsDates: true,
47
210
  supportsBooleans: true,
48
- supportsNumericIds: true
211
+ supportsNumericIds: true,
212
+ supportsUUIDs: true
49
213
  },
50
214
  adapter: () => {
51
215
  return {
52
216
  // ------------------------------------------------------------------
53
217
  // CREATE
54
218
  // ------------------------------------------------------------------
55
- create: async ({ model, data }) => {
56
- const { data: result, error } = await db.from(model).insert(data).select();
219
+ create: async ({
220
+ model,
221
+ data
222
+ }) => {
223
+ const db = ensureDbClient();
224
+ const insertData = toDbRecord(data);
225
+ const { data: result, error } = await db.from(model).insert(insertData).select();
57
226
  if (error) {
58
- throw new Error(`[AthenaAdapter] create on "${model}" failed: ${error}`);
227
+ throw new Error(
228
+ `[AthenaAdapter] create on "${model}" failed: ${error}`
229
+ );
59
230
  }
60
231
  const row = Array.isArray(result) ? result[0] : result;
61
- return row ?? data;
232
+ return mapRowToBetterAuth(row ?? insertData);
62
233
  },
63
234
  // ------------------------------------------------------------------
64
235
  // UPDATE
65
236
  // ------------------------------------------------------------------
66
- update: async ({ model, where, update }) => {
67
- let builder = db.from(model).update(update);
237
+ update: async ({
238
+ model,
239
+ where,
240
+ update
241
+ }) => {
242
+ const db = ensureDbClient();
243
+ const updateData = toDbRecord(update);
244
+ let builder = db.from(model).update(updateData);
68
245
  for (const clause of where) {
69
- builder = applyWhere(builder, clause.field, clause.operator, clause.value);
246
+ builder = applyWhere(
247
+ builder,
248
+ clause.field,
249
+ clause.operator,
250
+ clause.value
251
+ );
70
252
  }
71
253
  const { data: result, error } = await builder.select();
72
254
  if (error) {
73
- throw new Error(`[AthenaAdapter] update on "${model}" failed: ${error}`);
255
+ throw new Error(
256
+ `[AthenaAdapter] update on "${model}" failed: ${error}`
257
+ );
74
258
  }
75
259
  const row = Array.isArray(result) ? result[0] : result;
76
- return row ?? null;
260
+ return row ? mapRowToBetterAuth(row) : null;
77
261
  },
78
262
  // ------------------------------------------------------------------
79
263
  // UPDATE MANY
80
264
  // ------------------------------------------------------------------
81
- updateMany: async ({ model, where, update }) => {
82
- let builder = db.from(model).update(update);
265
+ updateMany: async ({
266
+ model,
267
+ where,
268
+ update
269
+ }) => {
270
+ const db = ensureDbClient();
271
+ const updateData = toDbRecord(update);
272
+ let builder = db.from(model).update(updateData);
83
273
  for (const clause of where) {
84
- builder = applyWhere(builder, clause.field, clause.operator, clause.value);
274
+ builder = applyWhere(
275
+ builder,
276
+ clause.field,
277
+ clause.operator,
278
+ clause.value
279
+ );
85
280
  }
86
281
  const { data: result, error } = await builder.select();
87
282
  if (error) {
88
- throw new Error(`[AthenaAdapter] updateMany on "${model}" failed: ${error}`);
283
+ throw new Error(
284
+ `[AthenaAdapter] updateMany on "${model}" failed: ${error}`
285
+ );
89
286
  }
90
287
  return Array.isArray(result) ? result.length : result ? 1 : 0;
91
288
  },
92
289
  // ------------------------------------------------------------------
93
290
  // DELETE
94
291
  // ------------------------------------------------------------------
95
- delete: async ({ model, where }) => {
292
+ delete: async ({
293
+ model,
294
+ where
295
+ }) => {
296
+ const db = ensureDbClient();
96
297
  let builder = db.from(model);
97
298
  for (const clause of where) {
98
- builder = applyWhere(builder, clause.field, clause.operator, clause.value);
299
+ builder = applyWhere(
300
+ builder,
301
+ clause.field,
302
+ clause.operator,
303
+ clause.value
304
+ );
99
305
  }
100
306
  const { error } = await builder.delete();
101
307
  if (error) {
102
- throw new Error(`[AthenaAdapter] delete on "${model}" failed: ${error}`);
308
+ throw new Error(
309
+ `[AthenaAdapter] delete on "${model}" failed: ${error}`
310
+ );
103
311
  }
104
312
  },
105
313
  // ------------------------------------------------------------------
106
314
  // DELETE MANY
107
315
  // ------------------------------------------------------------------
108
- deleteMany: async ({ model, where }) => {
316
+ deleteMany: async ({
317
+ model,
318
+ where
319
+ }) => {
320
+ const db = ensureDbClient();
109
321
  let builder = db.from(model);
110
322
  for (const clause of where) {
111
- builder = applyWhere(builder, clause.field, clause.operator, clause.value);
323
+ builder = applyWhere(
324
+ builder,
325
+ clause.field,
326
+ clause.operator,
327
+ clause.value
328
+ );
112
329
  }
113
330
  const { data: result, error } = await builder.delete().select();
114
331
  if (error) {
115
- throw new Error(`[AthenaAdapter] deleteMany on "${model}" failed: ${error}`);
332
+ throw new Error(
333
+ `[AthenaAdapter] deleteMany on "${model}" failed: ${error}`
334
+ );
116
335
  }
117
336
  return Array.isArray(result) ? result.length : result ? 1 : 0;
118
337
  },
119
338
  // ------------------------------------------------------------------
120
339
  // FIND ONE
121
340
  // ------------------------------------------------------------------
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}`);
341
+ findOne: async ({
342
+ model,
343
+ where,
344
+ select
345
+ }) => {
346
+ const db = ensureDbClient();
347
+ const snakeMapper = (col) => hasUppercase(col) ? toSnakeCase(col) : col;
348
+ const identityMapper = (col) => col;
349
+ const run = async (columnMapper) => {
350
+ const columns = select && select.length > 0 ? select.map((c) => columnMapper(c)).join(", ") : void 0;
351
+ let builder = db.from(model).select(columns);
352
+ for (const clause of where) {
353
+ builder = applyWhere(
354
+ builder,
355
+ clause.field,
356
+ clause.operator,
357
+ clause.value,
358
+ columnMapper
359
+ );
360
+ }
361
+ const { data: result, error } = await builder.limit(1);
362
+ return { result, error };
363
+ };
364
+ const first = await run(snakeMapper);
365
+ if (first.error) {
366
+ if (isMissingColumnError(first.error)) {
367
+ const retry = await run(identityMapper);
368
+ if (retry.error) {
369
+ throw new Error(
370
+ `[AthenaAdapter] findOne on "${model}" failed: ${retry.error}`
371
+ );
372
+ }
373
+ const rows2 = Array.isArray(retry.result) ? retry.result : retry.result ? [retry.result] : [];
374
+ const row2 = rows2[0] ?? null;
375
+ return row2 ? mapRowToBetterAuth(row2) : null;
376
+ }
377
+ throw new Error(
378
+ `[AthenaAdapter] findOne on "${model}" failed: ${first.error}`
379
+ );
131
380
  }
132
- const rows = Array.isArray(result) ? result : result ? [result] : [];
133
- return rows[0] ?? null;
381
+ const rows = Array.isArray(first.result) ? first.result : first.result ? [first.result] : [];
382
+ const row = rows[0] ?? null;
383
+ return row ? mapRowToBetterAuth(row) : null;
134
384
  },
135
385
  // ------------------------------------------------------------------
136
386
  // FIND MANY
137
387
  // ------------------------------------------------------------------
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);
388
+ findMany: async ({
389
+ model,
390
+ where,
391
+ limit,
392
+ sortBy,
393
+ offset,
394
+ select
395
+ }) => {
396
+ const db = ensureDbClient();
397
+ const snakeMapper = (col) => hasUppercase(col) ? toSnakeCase(col) : col;
398
+ const identityMapper = (col) => col;
399
+ const run = async (columnMapper) => {
400
+ const columns = select && select.length > 0 ? select.map((c) => columnMapper(c)).join(", ") : void 0;
401
+ let builder = db.from(model).select(columns);
402
+ if (where) {
403
+ for (const clause of where) {
404
+ builder = applyWhere(
405
+ builder,
406
+ clause.field,
407
+ clause.operator,
408
+ clause.value,
409
+ columnMapper
410
+ );
411
+ }
144
412
  }
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) {
413
+ if (limit !== void 0) {
414
+ builder = builder.limit(limit);
415
+ }
416
+ if (offset !== void 0) {
417
+ builder = builder.offset(offset);
418
+ }
419
+ const { data: result, error } = await builder;
420
+ return { result, error };
421
+ };
422
+ const first = await run(snakeMapper);
423
+ const pickRows = (res) => Array.isArray(res) ? res : [];
424
+ const applySort = (rows) => {
425
+ if (!sortBy) return rows;
426
+ const sortField = sortBy.field;
158
427
  rows.sort((a, b) => {
159
- const aVal = a[sortBy.field];
160
- const bVal = b[sortBy.field];
428
+ const aVal = a[sortField];
429
+ const bVal = b[sortField];
161
430
  if (aVal == null && bVal == null) return 0;
162
431
  if (aVal == null) return sortBy.direction === "asc" ? -1 : 1;
163
432
  if (bVal == null) return sortBy.direction === "asc" ? 1 : -1;
164
433
  const cmp = typeof aVal === "string" && typeof bVal === "string" ? aVal.localeCompare(bVal) : aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
165
434
  return sortBy.direction === "asc" ? cmp : -cmp;
166
435
  });
436
+ return rows;
437
+ };
438
+ const mapAndSort = (rows) => {
439
+ const betterAuthRows = rows.map(
440
+ (r) => mapRowToBetterAuth(r)
441
+ );
442
+ return applySort(betterAuthRows);
443
+ };
444
+ if (first.error) {
445
+ if (isMissingColumnError(first.error)) {
446
+ const retry = await run(identityMapper);
447
+ if (retry.error) {
448
+ throw new Error(
449
+ `[AthenaAdapter] findMany on "${model}" failed: ${retry.error}`
450
+ );
451
+ }
452
+ return mapAndSort(pickRows(retry.result));
453
+ }
454
+ throw new Error(
455
+ `[AthenaAdapter] findMany on "${model}" failed: ${first.error}`
456
+ );
167
457
  }
168
- return rows;
458
+ return mapAndSort(pickRows(first.result));
169
459
  },
170
460
  // ------------------------------------------------------------------
171
461
  // COUNT
172
462
  // ------------------------------------------------------------------
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);
463
+ count: async ({
464
+ model,
465
+ where
466
+ }) => {
467
+ const db = ensureDbClient();
468
+ const snakeMapper = (col) => hasUppercase(col) ? toSnakeCase(col) : col;
469
+ const identityMapper = (col) => col;
470
+ const run = async (columnMapper) => {
471
+ let builder = db.from(model).select();
472
+ if (where) {
473
+ for (const clause of where) {
474
+ builder = applyWhere(
475
+ builder,
476
+ clause.field,
477
+ clause.operator,
478
+ clause.value,
479
+ columnMapper
480
+ );
481
+ }
178
482
  }
483
+ const { data: result, error } = await builder;
484
+ return { result, error };
485
+ };
486
+ const first = await run(snakeMapper);
487
+ if (first.error) {
488
+ if (isMissingColumnError(first.error)) {
489
+ const retry = await run(identityMapper);
490
+ if (retry.error) {
491
+ throw new Error(
492
+ `[AthenaAdapter] count on "${model}" failed: ${retry.error}`
493
+ );
494
+ }
495
+ return Array.isArray(retry.result) ? retry.result.length : 0;
496
+ }
497
+ throw new Error(
498
+ `[AthenaAdapter] count on "${model}" failed: ${first.error}`
499
+ );
179
500
  }
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;
501
+ return Array.isArray(first.result) ? first.result.length : 0;
185
502
  }
186
503
  };
187
504
  }