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