@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 +7 -4
- package/dist/index.cjs +404 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +397 -80
- package/dist/index.js.map +1 -1
- package/package.json +67 -65
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` |
|
|
39
|
-
| `apiKey` | `string` |
|
|
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
|
-
|
|
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(
|
|
171
|
+
return builder.eq(dbField, value);
|
|
32
172
|
case "ne":
|
|
33
|
-
return builder.neq(
|
|
173
|
+
return builder.neq(dbField, value);
|
|
34
174
|
case "gt":
|
|
35
|
-
return builder.gt(
|
|
175
|
+
return builder.gt(dbField, value);
|
|
36
176
|
case "gte":
|
|
37
|
-
return builder.gte(
|
|
177
|
+
return builder.gte(dbField, value);
|
|
38
178
|
case "lt":
|
|
39
|
-
return builder.lt(
|
|
179
|
+
return builder.lt(dbField, value);
|
|
40
180
|
case "lte":
|
|
41
|
-
return builder.lte(
|
|
181
|
+
return builder.lte(dbField, value);
|
|
42
182
|
case "in":
|
|
43
|
-
return builder.in(
|
|
183
|
+
return builder.in(dbField, value);
|
|
44
184
|
case "not_in":
|
|
45
|
-
return builder.not(
|
|
185
|
+
return builder.not(dbField, "in", value);
|
|
46
186
|
case "contains":
|
|
47
|
-
return builder.like(
|
|
187
|
+
return builder.like(dbField, `%${value}%`);
|
|
48
188
|
case "starts_with":
|
|
49
|
-
return builder.like(
|
|
189
|
+
return builder.like(dbField, `${value}%`);
|
|
50
190
|
case "ends_with":
|
|
51
|
-
return builder.like(
|
|
191
|
+
return builder.like(dbField, `%${value}`);
|
|
52
192
|
default:
|
|
53
|
-
return builder.eq(
|
|
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
|
-
|
|
58
|
-
|
|
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 ({
|
|
78
|
-
|
|
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(
|
|
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 ??
|
|
262
|
+
return mapRowToBetterAuth(row ?? insertData);
|
|
84
263
|
},
|
|
85
264
|
// ------------------------------------------------------------------
|
|
86
265
|
// UPDATE
|
|
87
266
|
// ------------------------------------------------------------------
|
|
88
|
-
update: async ({
|
|
89
|
-
|
|
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(
|
|
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(
|
|
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
|
|
290
|
+
return row ? mapRowToBetterAuth(row) : null;
|
|
99
291
|
},
|
|
100
292
|
// ------------------------------------------------------------------
|
|
101
293
|
// UPDATE MANY
|
|
102
294
|
// ------------------------------------------------------------------
|
|
103
|
-
updateMany: async ({
|
|
104
|
-
|
|
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(
|
|
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(
|
|
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 ({
|
|
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(
|
|
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(
|
|
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 ({
|
|
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(
|
|
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(
|
|
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 ({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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 ({
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
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[
|
|
182
|
-
const bVal = b[
|
|
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
|
|
488
|
+
return mapAndSort(pickRows(first.result));
|
|
191
489
|
},
|
|
192
490
|
// ------------------------------------------------------------------
|
|
193
491
|
// COUNT
|
|
194
492
|
// ------------------------------------------------------------------
|
|
195
|
-
count: async ({
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
}
|