@better-auth/memory-adapter 1.7.0-beta.4 → 1.7.0-beta.6
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 +109 -27
- 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) => {
|
|
@@ -152,9 +224,9 @@ const memoryAdapter = (db, config) => {
|
|
|
152
224
|
const nestedEntry = grouped.get(baseId);
|
|
153
225
|
for (const [joinModel, joinAttr] of Object.entries(join)) {
|
|
154
226
|
const joinModelName = getModelName(joinModel);
|
|
155
|
-
const joinTable =
|
|
227
|
+
const joinTable = activeDb[joinModelName];
|
|
156
228
|
if (!joinTable) {
|
|
157
|
-
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));
|
|
158
230
|
throw new Error(`JoinOption model ${joinModelName} not found`);
|
|
159
231
|
}
|
|
160
232
|
const matchingRecords = joinTable.filter((joinRecord) => joinRecord[joinAttr.on.to] === baseRecord[joinAttr.on.from]);
|
|
@@ -178,9 +250,9 @@ const memoryAdapter = (db, config) => {
|
|
|
178
250
|
}
|
|
179
251
|
return {
|
|
180
252
|
create: async ({ model, data }) => {
|
|
181
|
-
if (options.advanced?.database?.generateId === "serial") data.id =
|
|
182
|
-
if (!
|
|
183
|
-
|
|
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);
|
|
184
256
|
return data;
|
|
185
257
|
},
|
|
186
258
|
findOne: async ({ model, where, select, join }) => {
|
|
@@ -210,9 +282,10 @@ const memoryAdapter = (db, config) => {
|
|
|
210
282
|
},
|
|
211
283
|
count: async ({ model, where }) => {
|
|
212
284
|
if (where) return convertWhereClause(where, model).length;
|
|
213
|
-
return
|
|
285
|
+
return activeDb[model].length;
|
|
214
286
|
},
|
|
215
287
|
update: async ({ model, where, update }) => {
|
|
288
|
+
if (where.length === 0) return null;
|
|
216
289
|
const res = convertWhereClause(where, model);
|
|
217
290
|
res.forEach((record) => {
|
|
218
291
|
Object.assign(record, update);
|
|
@@ -220,15 +293,16 @@ const memoryAdapter = (db, config) => {
|
|
|
220
293
|
return res[0] || null;
|
|
221
294
|
},
|
|
222
295
|
delete: async ({ model, where }) => {
|
|
223
|
-
|
|
296
|
+
if (where.length === 0) return;
|
|
297
|
+
const table = activeDb[model];
|
|
224
298
|
const res = convertWhereClause(where, model);
|
|
225
|
-
|
|
299
|
+
activeDb[model] = table.filter((record) => !res.includes(record));
|
|
226
300
|
},
|
|
227
301
|
deleteMany: async ({ model, where }) => {
|
|
228
|
-
const table =
|
|
302
|
+
const table = activeDb[model];
|
|
229
303
|
const res = convertWhereClause(where, model);
|
|
230
304
|
let count = 0;
|
|
231
|
-
|
|
305
|
+
activeDb[model] = table.filter((record) => {
|
|
232
306
|
if (res.includes(record)) {
|
|
233
307
|
count++;
|
|
234
308
|
return false;
|
|
@@ -238,22 +312,30 @@ const memoryAdapter = (db, config) => {
|
|
|
238
312
|
return count;
|
|
239
313
|
},
|
|
240
314
|
consumeOne: async ({ model, where }) => {
|
|
241
|
-
const table =
|
|
315
|
+
const table = activeDb[model];
|
|
242
316
|
const target = convertWhereClause(where, model)[0];
|
|
243
317
|
if (!target) return null;
|
|
244
|
-
|
|
318
|
+
activeDb[model] = table.filter((record) => record !== target);
|
|
245
319
|
return target;
|
|
246
320
|
},
|
|
247
|
-
|
|
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 }) => {
|
|
248
329
|
const res = convertWhereClause(where, model);
|
|
249
330
|
res.forEach((record) => {
|
|
250
331
|
Object.assign(record, update);
|
|
251
332
|
});
|
|
252
|
-
return res
|
|
333
|
+
return res.length;
|
|
253
334
|
}
|
|
254
335
|
};
|
|
255
336
|
}
|
|
256
337
|
});
|
|
338
|
+
const adapterCreator = buildAdapterFactory(db);
|
|
257
339
|
return (options) => {
|
|
258
340
|
lazyOptions = options;
|
|
259
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.6",
|
|
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.6"
|
|
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.6"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsdown",
|