@better-auth/memory-adapter 1.7.0-beta.1 → 1.7.0-beta.10
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.mjs +118 -26
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -30,9 +30,85 @@ function insensitiveEndsWith(recordVal, value) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/memory-adapter.ts
|
|
33
|
+
/**
|
|
34
|
+
* Index a table's rows by their `id` for row-level reconciliation. Every
|
|
35
|
+
* better-auth row carries an `id` (the adapter's join logic already keys on
|
|
36
|
+
* `record.id`), so the id is a stable identity for the three-way merge.
|
|
37
|
+
*/
|
|
38
|
+
function indexById(rows) {
|
|
39
|
+
const byId = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const row of rows) byId.set(row.id, row);
|
|
41
|
+
return byId;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Commit a transaction onto the live database with a three-way merge so a
|
|
45
|
+
* concurrent write that interleaved at an `await` point survives.
|
|
46
|
+
*
|
|
47
|
+
* `base` is the snapshot taken when the transaction started; `clone` is that
|
|
48
|
+
* snapshot after the transaction mutated it; `target` is the live database as
|
|
49
|
+
* it stands now (possibly carrying concurrent writes). Replaying only the
|
|
50
|
+
* `base -> clone` delta onto `target` applies the transaction's own creates,
|
|
51
|
+
* updates, and deletes without disturbing rows or tables the transaction never
|
|
52
|
+
* touched. A row the transaction did not change keeps the live version, so a
|
|
53
|
+
* concurrent edit to a different row is preserved. A row the transaction did
|
|
54
|
+
* change wins last-writer-wins over a concurrent edit to the same row, which is
|
|
55
|
+
* acceptable for an in-memory development adapter; isolation is guaranteed only
|
|
56
|
+
* at row/table granularity.
|
|
57
|
+
*
|
|
58
|
+
* `target` is mutated in place so any reference held elsewhere (for example the
|
|
59
|
+
* `db` object the caller passed to `memoryAdapter`) stays valid.
|
|
60
|
+
*/
|
|
61
|
+
function mergeTransactionInto(target, base, clone) {
|
|
62
|
+
const models = new Set([...Object.keys(base), ...Object.keys(clone)]);
|
|
63
|
+
for (const model of models) {
|
|
64
|
+
if (!(model in clone)) {
|
|
65
|
+
delete target[model];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const baseById = indexById(base[model] ?? []);
|
|
69
|
+
const cloneRows = clone[model] ?? [];
|
|
70
|
+
const cloneById = indexById(cloneRows);
|
|
71
|
+
const liveRows = target[model] ?? [];
|
|
72
|
+
const merged = [];
|
|
73
|
+
const placed = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const liveRow of liveRows) {
|
|
75
|
+
const id = liveRow.id;
|
|
76
|
+
const baseRow = baseById.get(id);
|
|
77
|
+
const cloneRow = cloneById.get(id);
|
|
78
|
+
if (baseRow !== void 0 && cloneRow === void 0) continue;
|
|
79
|
+
if (cloneRow !== void 0 && rowChanged(baseRow, cloneRow)) merged.push(cloneRow);
|
|
80
|
+
else merged.push(liveRow);
|
|
81
|
+
placed.add(id);
|
|
82
|
+
}
|
|
83
|
+
for (const cloneRow of cloneRows) if (!baseById.has(cloneRow.id) && !placed.has(cloneRow.id)) merged.push(cloneRow);
|
|
84
|
+
target[model] = merged;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether the transaction mutated a row, comparing its pre-transaction
|
|
89
|
+
* snapshot to its post-transaction state. Rows hold scalar columns and
|
|
90
|
+
* adapter-serializable values, so JSON equality reliably tells a
|
|
91
|
+
* transaction-made edit apart from an untouched row.
|
|
92
|
+
*/
|
|
93
|
+
function rowChanged(baseRow, cloneRow) {
|
|
94
|
+
if (baseRow === void 0) return true;
|
|
95
|
+
return JSON.stringify(baseRow) !== JSON.stringify(cloneRow);
|
|
96
|
+
}
|
|
33
97
|
const memoryAdapter = (db, config) => {
|
|
34
98
|
let lazyOptions = null;
|
|
35
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Build an adapter factory whose operations read and write `activeDb`.
|
|
101
|
+
* The non-transactional adapter targets the live `db`. A transaction
|
|
102
|
+
* targets an isolated clone so its uncommitted writes are invisible to
|
|
103
|
+
* concurrent operations against the live `db`. A failed transaction leaves
|
|
104
|
+
* the live `db` untouched, and a committed one replays only its own
|
|
105
|
+
* row/table changes, so a concurrent write that interleaved at an `await`
|
|
106
|
+
* point survives either outcome. Isolation is at row/table granularity:
|
|
107
|
+
* the in-memory adapter does not serialize writes, so two operations that
|
|
108
|
+
* edit the same row resolve last-writer-wins. It is built for development
|
|
109
|
+
* and tests, not production concurrency control.
|
|
110
|
+
*/
|
|
111
|
+
const buildAdapterFactory = (activeDb) => createAdapterFactory({
|
|
36
112
|
config: {
|
|
37
113
|
adapterId: "memory",
|
|
38
114
|
adapterName: "Memory Adapter",
|
|
@@ -40,19 +116,15 @@ const memoryAdapter = (db, config) => {
|
|
|
40
116
|
debugLogs: config?.debugLogs || false,
|
|
41
117
|
supportsArrays: true,
|
|
42
118
|
customTransformInput(props) {
|
|
43
|
-
if (props.options.advanced?.database?.generateId === "serial" && props.field === "id" && props.action === "create") return
|
|
119
|
+
if (props.options.advanced?.database?.generateId === "serial" && props.field === "id" && props.action === "create") return activeDb[props.model].length + 1;
|
|
44
120
|
return props.data;
|
|
45
121
|
},
|
|
46
122
|
transaction: async (cb) => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
db[key] = clone[key];
|
|
53
|
-
});
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
123
|
+
const base = structuredClone(activeDb);
|
|
124
|
+
const clone = structuredClone(activeDb);
|
|
125
|
+
const result = await cb(buildAdapterFactory(clone)(lazyOptions));
|
|
126
|
+
mergeTransactionInto(activeDb, base, clone);
|
|
127
|
+
return result;
|
|
56
128
|
}
|
|
57
129
|
},
|
|
58
130
|
adapter: ({ getFieldName, getDefaultFieldName, options, getModelName }) => {
|
|
@@ -79,9 +151,9 @@ const memoryAdapter = (db, config) => {
|
|
|
79
151
|
};
|
|
80
152
|
function convertWhereClause(where, model, join, select) {
|
|
81
153
|
const baseRecords = (() => {
|
|
82
|
-
const table =
|
|
154
|
+
const table = activeDb[model];
|
|
83
155
|
if (!table) {
|
|
84
|
-
logger.error(`[MemoryAdapter] Model ${model} not found in the DB`, Object.keys(
|
|
156
|
+
logger.error(`[MemoryAdapter] Model ${model} not found in the DB`, Object.keys(activeDb));
|
|
85
157
|
throw new Error(`Model ${model} not found`);
|
|
86
158
|
}
|
|
87
159
|
const evalClause = (record, clause) => {
|
|
@@ -110,7 +182,10 @@ const memoryAdapter = (db, config) => {
|
|
|
110
182
|
case "gte": return value != null && Boolean(record[field] >= value);
|
|
111
183
|
case "lt": return value != null && Boolean(record[field] < value);
|
|
112
184
|
case "lte": return value != null && Boolean(record[field] <= value);
|
|
113
|
-
default:
|
|
185
|
+
default:
|
|
186
|
+
if (isInsensitive) return insensitiveCompare(record[field], value);
|
|
187
|
+
if (value === null) return record[field] == null;
|
|
188
|
+
return record[field] === value;
|
|
114
189
|
}
|
|
115
190
|
};
|
|
116
191
|
let records = table.filter((record) => {
|
|
@@ -149,9 +224,9 @@ const memoryAdapter = (db, config) => {
|
|
|
149
224
|
const nestedEntry = grouped.get(baseId);
|
|
150
225
|
for (const [joinModel, joinAttr] of Object.entries(join)) {
|
|
151
226
|
const joinModelName = getModelName(joinModel);
|
|
152
|
-
const joinTable =
|
|
227
|
+
const joinTable = activeDb[joinModelName];
|
|
153
228
|
if (!joinTable) {
|
|
154
|
-
logger.error(`[MemoryAdapter] JoinOption model ${joinModelName} not found in the DB`, Object.keys(
|
|
229
|
+
logger.error(`[MemoryAdapter] JoinOption model ${joinModelName} not found in the DB`, Object.keys(activeDb));
|
|
155
230
|
throw new Error(`JoinOption model ${joinModelName} not found`);
|
|
156
231
|
}
|
|
157
232
|
const matchingRecords = joinTable.filter((joinRecord) => joinRecord[joinAttr.on.to] === baseRecord[joinAttr.on.from]);
|
|
@@ -175,9 +250,9 @@ const memoryAdapter = (db, config) => {
|
|
|
175
250
|
}
|
|
176
251
|
return {
|
|
177
252
|
create: async ({ model, data }) => {
|
|
178
|
-
if (options.advanced?.database?.generateId === "serial") data.id =
|
|
179
|
-
if (!
|
|
180
|
-
|
|
253
|
+
if (options.advanced?.database?.generateId === "serial") data.id = activeDb[getModelName(model)].length + 1;
|
|
254
|
+
if (!activeDb[model]) activeDb[model] = [];
|
|
255
|
+
activeDb[model].push(data);
|
|
181
256
|
return data;
|
|
182
257
|
},
|
|
183
258
|
findOne: async ({ model, where, select, join }) => {
|
|
@@ -207,9 +282,10 @@ const memoryAdapter = (db, config) => {
|
|
|
207
282
|
},
|
|
208
283
|
count: async ({ model, where }) => {
|
|
209
284
|
if (where) return convertWhereClause(where, model).length;
|
|
210
|
-
return
|
|
285
|
+
return activeDb[model].length;
|
|
211
286
|
},
|
|
212
287
|
update: async ({ model, where, update }) => {
|
|
288
|
+
if (where.length === 0) return null;
|
|
213
289
|
const res = convertWhereClause(where, model);
|
|
214
290
|
res.forEach((record) => {
|
|
215
291
|
Object.assign(record, update);
|
|
@@ -217,15 +293,16 @@ const memoryAdapter = (db, config) => {
|
|
|
217
293
|
return res[0] || null;
|
|
218
294
|
},
|
|
219
295
|
delete: async ({ model, where }) => {
|
|
220
|
-
|
|
296
|
+
if (where.length === 0) return;
|
|
297
|
+
const table = activeDb[model];
|
|
221
298
|
const res = convertWhereClause(where, model);
|
|
222
|
-
|
|
299
|
+
activeDb[model] = table.filter((record) => !res.includes(record));
|
|
223
300
|
},
|
|
224
301
|
deleteMany: async ({ model, where }) => {
|
|
225
|
-
const table =
|
|
302
|
+
const table = activeDb[model];
|
|
226
303
|
const res = convertWhereClause(where, model);
|
|
227
304
|
let count = 0;
|
|
228
|
-
|
|
305
|
+
activeDb[model] = table.filter((record) => {
|
|
229
306
|
if (res.includes(record)) {
|
|
230
307
|
count++;
|
|
231
308
|
return false;
|
|
@@ -234,16 +311,31 @@ const memoryAdapter = (db, config) => {
|
|
|
234
311
|
});
|
|
235
312
|
return count;
|
|
236
313
|
},
|
|
237
|
-
|
|
314
|
+
consumeOne: async ({ model, where }) => {
|
|
315
|
+
const table = activeDb[model];
|
|
316
|
+
const target = convertWhereClause(where, model)[0];
|
|
317
|
+
if (!target) return null;
|
|
318
|
+
activeDb[model] = table.filter((record) => record !== target);
|
|
319
|
+
return target;
|
|
320
|
+
},
|
|
321
|
+
incrementOne: async ({ model, where, increment, set }) => {
|
|
322
|
+
const target = convertWhereClause(where, model)[0];
|
|
323
|
+
if (!target) return null;
|
|
324
|
+
for (const [field, delta] of Object.entries(increment)) target[field] = (typeof target[field] === "number" ? target[field] : 0) + delta;
|
|
325
|
+
if (set) Object.assign(target, set);
|
|
326
|
+
return target;
|
|
327
|
+
},
|
|
328
|
+
updateMany: async ({ model, where, update }) => {
|
|
238
329
|
const res = convertWhereClause(where, model);
|
|
239
330
|
res.forEach((record) => {
|
|
240
331
|
Object.assign(record, update);
|
|
241
332
|
});
|
|
242
|
-
return res
|
|
333
|
+
return res.length;
|
|
243
334
|
}
|
|
244
335
|
};
|
|
245
336
|
}
|
|
246
337
|
});
|
|
338
|
+
const adapterCreator = buildAdapterFactory(db);
|
|
247
339
|
return (options) => {
|
|
248
340
|
lazyOptions = options;
|
|
249
341
|
return adapterCreator(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/memory-adapter",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.10",
|
|
4
4
|
"description": "Memory adapter for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@better-auth/utils": "0.4.
|
|
39
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
38
|
+
"@better-auth/utils": "0.4.2",
|
|
39
|
+
"@better-auth/core": "^1.7.0-beta.10"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@better-auth/utils": "0.4.
|
|
42
|
+
"@better-auth/utils": "0.4.2",
|
|
43
43
|
"tsdown": "0.21.1",
|
|
44
44
|
"typescript": "^5.9.3",
|
|
45
|
-
"@better-auth/core": "1.7.0-beta.
|
|
45
|
+
"@better-auth/core": "1.7.0-beta.10"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsdown",
|