@datar-platform/better-auth-dynamodb 0.1.1 → 0.2.0
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/CHANGELOG.md +101 -1
- package/README.md +75 -10
- package/dist/index.d.ts +155 -3
- package/dist/index.js +636 -95
- package/dist/index.js.map +1 -1
- package/package.json +11 -8
package/dist/index.js
CHANGED
|
@@ -1,20 +1,79 @@
|
|
|
1
1
|
// src/adapter.ts
|
|
2
2
|
import { createAdapterFactory } from "better-auth/adapters";
|
|
3
3
|
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var DynamoDBAdapterError = class extends Error {
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.name = "DynamoDBAdapterError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var UniqueConstraintError = class extends DynamoDBAdapterError {
|
|
12
|
+
constructor(model, fields, options) {
|
|
13
|
+
super(
|
|
14
|
+
`better-auth-dynamodb: unique constraint violation on "${model}" (duplicate value for ${fields.join(", ")})`,
|
|
15
|
+
options
|
|
16
|
+
);
|
|
17
|
+
this.model = model;
|
|
18
|
+
this.fields = fields;
|
|
19
|
+
this.name = "UniqueConstraintError";
|
|
20
|
+
}
|
|
21
|
+
model;
|
|
22
|
+
fields;
|
|
23
|
+
};
|
|
24
|
+
var UnsupportedQueryError = class extends DynamoDBAdapterError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "UnsupportedQueryError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var OptimisticLockError = class extends DynamoDBAdapterError {
|
|
31
|
+
constructor(model, id, options) {
|
|
32
|
+
super(
|
|
33
|
+
`better-auth-dynamodb: concurrent modification of "${model}" row "${id}" \u2014 the record changed between read and write`,
|
|
34
|
+
options
|
|
35
|
+
);
|
|
36
|
+
this.name = "OptimisticLockError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var named = (error, name) => typeof error === "object" && error !== null && "name" in error && error.name === name;
|
|
40
|
+
var isConditionalCheckFailed = (error) => named(error, "ConditionalCheckFailedException");
|
|
41
|
+
var isTransactionCanceled = (error) => named(error, "TransactionCanceledException");
|
|
42
|
+
function transactionCancellationCodes(error) {
|
|
43
|
+
if (typeof error !== "object" || error === null || !("CancellationReasons" in error)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const reasons = error.CancellationReasons;
|
|
47
|
+
if (!Array.isArray(reasons)) return [];
|
|
48
|
+
return reasons.map(
|
|
49
|
+
(reason) => typeof reason === "object" && reason !== null && "Code" in reason ? reason.Code : void 0
|
|
50
|
+
).filter((code) => typeof code === "string");
|
|
51
|
+
}
|
|
52
|
+
var isConditionalTransactionCanceled = (error) => isTransactionCanceled(error) && transactionCancellationCodes(error).includes("ConditionalCheckFailed");
|
|
53
|
+
var isTransactionConflict = (error) => isTransactionCanceled(error) && transactionCancellationCodes(error).includes("TransactionConflict");
|
|
54
|
+
|
|
4
55
|
// src/pagination.ts
|
|
5
|
-
|
|
56
|
+
var DEFAULT_MAX_PAGES = 25;
|
|
57
|
+
async function drainPages(fetch, maxPages = DEFAULT_MAX_PAGES, label = "query") {
|
|
6
58
|
const out = [];
|
|
7
59
|
let cursor = void 0;
|
|
60
|
+
let pages = 0;
|
|
8
61
|
do {
|
|
9
62
|
const page = await fetch(cursor);
|
|
10
63
|
out.push(...page.items);
|
|
11
64
|
cursor = page.cursor;
|
|
65
|
+
if (++pages >= maxPages && cursor) {
|
|
66
|
+
throw new DynamoDBAdapterError(
|
|
67
|
+
`better-auth-dynamodb: ${label} exceeded maxPages (${maxPages}) with more pages remaining. Raise maxPages or narrow the query \u2014 returning a partial result here would silently drop rows.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
12
70
|
} while (cursor);
|
|
13
71
|
return out;
|
|
14
72
|
}
|
|
15
73
|
function matchOne(item, w) {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
74
|
+
const fold = (v) => w.mode === "insensitive" && typeof v === "string" ? v.toLowerCase() : v;
|
|
75
|
+
const actual = fold(item[w.field]);
|
|
76
|
+
const expected = Array.isArray(w.value) ? w.value.map(fold) : fold(w.value);
|
|
18
77
|
switch (w.operator) {
|
|
19
78
|
case "eq":
|
|
20
79
|
return actual === expected;
|
|
@@ -67,7 +126,7 @@ function applyWindow(items, offset, limit) {
|
|
|
67
126
|
|
|
68
127
|
// src/planner.ts
|
|
69
128
|
function isKeyable(w) {
|
|
70
|
-
return w.operator === "eq" && w.connector === "AND" && w.value != null;
|
|
129
|
+
return w.operator === "eq" && w.connector === "AND" && w.value != null && w.mode !== "insensitive";
|
|
71
130
|
}
|
|
72
131
|
function planQuery(model, where, indexMap) {
|
|
73
132
|
const eqByField = /* @__PURE__ */ new Map();
|
|
@@ -82,6 +141,16 @@ function planQuery(model, where, indexMap) {
|
|
|
82
141
|
residual: where.filter((w) => w !== idClause)
|
|
83
142
|
};
|
|
84
143
|
}
|
|
144
|
+
const idIn = where.find(
|
|
145
|
+
(w) => w.field === "id" && w.operator === "in" && w.connector === "AND" && Array.isArray(w.value)
|
|
146
|
+
);
|
|
147
|
+
if (idIn) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "byIds",
|
|
150
|
+
ids: idIn.value.map(String),
|
|
151
|
+
residual: where.filter((w) => w !== idIn)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
85
154
|
const patterns = indexMap[model] ?? [];
|
|
86
155
|
for (const pattern of patterns) {
|
|
87
156
|
if (!pattern.pk.every((f) => eqByField.has(f))) continue;
|
|
@@ -117,13 +186,13 @@ function deriveIndexMap(schema) {
|
|
|
117
186
|
for (const [model, table] of Object.entries(schema)) {
|
|
118
187
|
const patterns = [];
|
|
119
188
|
const seen = /* @__PURE__ */ new Set();
|
|
120
|
-
const add = (field) => {
|
|
189
|
+
const add = (field, unique = false) => {
|
|
121
190
|
if (field === "id" || seen.has(field)) return;
|
|
122
191
|
seen.add(field);
|
|
123
|
-
patterns.push({ index: lookupIndexName(field), pk: [field] });
|
|
192
|
+
patterns.push({ index: lookupIndexName(field), pk: [field], unique });
|
|
124
193
|
};
|
|
125
194
|
for (const [field, attr] of Object.entries(table.fields)) {
|
|
126
|
-
if (attr.unique) add(field);
|
|
195
|
+
if (attr.unique) add(field, true);
|
|
127
196
|
}
|
|
128
197
|
for (const [field, attr] of Object.entries(table.fields)) {
|
|
129
198
|
if (attr.references) add(field);
|
|
@@ -137,6 +206,7 @@ function deriveIndexMap(schema) {
|
|
|
137
206
|
}
|
|
138
207
|
|
|
139
208
|
// src/stores/default/single-table-store.ts
|
|
209
|
+
import { randomUUID } from "crypto";
|
|
140
210
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
141
211
|
import {
|
|
142
212
|
DeleteCommand,
|
|
@@ -144,22 +214,68 @@ import {
|
|
|
144
214
|
GetCommand,
|
|
145
215
|
PutCommand,
|
|
146
216
|
QueryCommand,
|
|
217
|
+
TransactWriteCommand,
|
|
147
218
|
UpdateCommand
|
|
148
219
|
} from "@aws-sdk/lib-dynamodb";
|
|
149
220
|
|
|
150
221
|
// src/stores/default/key-codec.ts
|
|
222
|
+
import { createHash } from "crypto";
|
|
151
223
|
var PK = "__ba_pk";
|
|
152
224
|
var SK = "__ba_sk";
|
|
153
225
|
var TYPE_PK = "__ba_tpk";
|
|
154
226
|
var TYPE_SK = "__ba_tsk";
|
|
155
227
|
var TYPE_INDEX = "byType";
|
|
228
|
+
var REVISION = "__ba_rev";
|
|
229
|
+
var DEFAULT_TTL_ATTRIBUTE = "__ba_ttl";
|
|
156
230
|
var SK_CONST = "#";
|
|
157
231
|
var RESERVED_PREFIX = "__ba_";
|
|
232
|
+
var ENTITY_PREFIX = "E";
|
|
233
|
+
var LOOKUP_PREFIX = "L";
|
|
234
|
+
var UNIQUE_PREFIX = "U";
|
|
235
|
+
var MAX_PARTITION_KEY_BYTES = 2048;
|
|
236
|
+
var MAX_SORT_KEY_BYTES = 1024;
|
|
237
|
+
var MAX_VALUE_COMPONENT_BYTES = 256;
|
|
158
238
|
var gsiName = (slot) => `lookup${slot}`;
|
|
159
239
|
var gsiPk = (slot) => `__ba_g${slot}pk`;
|
|
160
240
|
var gsiSk = (slot) => `__ba_g${slot}sk`;
|
|
161
|
-
var
|
|
162
|
-
var
|
|
241
|
+
var byteLength = (value) => Buffer.byteLength(value, "utf8");
|
|
242
|
+
var component = (value) => `s${byteLength(value)}:${value}`;
|
|
243
|
+
function encodeValue(value) {
|
|
244
|
+
if (value === null || value === void 0) return "null:";
|
|
245
|
+
if (value instanceof Date) return `date:${value.toISOString()}`;
|
|
246
|
+
switch (typeof value) {
|
|
247
|
+
case "string":
|
|
248
|
+
return `string:${value}`;
|
|
249
|
+
case "number":
|
|
250
|
+
return `number:${value}`;
|
|
251
|
+
case "boolean":
|
|
252
|
+
return `boolean:${value}`;
|
|
253
|
+
case "bigint":
|
|
254
|
+
return `bigint:${value}`;
|
|
255
|
+
case "symbol":
|
|
256
|
+
return `symbol:${value.description ?? ""}`;
|
|
257
|
+
case "function":
|
|
258
|
+
return "function:";
|
|
259
|
+
default:
|
|
260
|
+
return `object:${JSON.stringify(value)}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function valueComponent(value) {
|
|
264
|
+
const typed = encodeValue(value);
|
|
265
|
+
if (byteLength(typed) > MAX_VALUE_COMPONENT_BYTES) {
|
|
266
|
+
return `hs:${createHash("sha256").update(typed).digest("hex")}`;
|
|
267
|
+
}
|
|
268
|
+
return component(typed);
|
|
269
|
+
}
|
|
270
|
+
var joinValues = (values) => values.map(valueComponent).join("#");
|
|
271
|
+
function checkedKey(value, label, maxBytes, keyType) {
|
|
272
|
+
if (byteLength(value) <= maxBytes) return value;
|
|
273
|
+
throw new DynamoDBAdapterError(
|
|
274
|
+
`better-auth-dynamodb: ${label} exceeds DynamoDB's ${maxBytes}-byte ${keyType} key limit. Field values are hashed past ${MAX_VALUE_COMPONENT_BYTES} bytes, so this means the model name, index name, or record id is itself too long.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
var partitionKey = (value, label) => checkedKey(value, label, MAX_PARTITION_KEY_BYTES, "partition");
|
|
278
|
+
var sortKey = (value, label) => checkedKey(value, label, MAX_SORT_KEY_BYTES, "sort");
|
|
163
279
|
function assignSlots(indexMap) {
|
|
164
280
|
const slots = {};
|
|
165
281
|
let maxSlots = 0;
|
|
@@ -174,16 +290,36 @@ function assignSlots(indexMap) {
|
|
|
174
290
|
return { slots, maxSlots };
|
|
175
291
|
}
|
|
176
292
|
var primaryKey = (model, id) => ({
|
|
177
|
-
[PK]:
|
|
293
|
+
[PK]: partitionKey(
|
|
294
|
+
`${ENTITY_PREFIX}#${component(model)}#${component(id)}`,
|
|
295
|
+
"entity partition key"
|
|
296
|
+
),
|
|
178
297
|
[SK]: SK_CONST
|
|
179
298
|
});
|
|
180
|
-
var
|
|
299
|
+
var uniqueMarkerKey = (model, index, values) => ({
|
|
300
|
+
[PK]: partitionKey(
|
|
301
|
+
`${UNIQUE_PREFIX}#${component(model)}#${component(index)}#${joinValues(values)}`,
|
|
302
|
+
"unique marker partition key"
|
|
303
|
+
),
|
|
304
|
+
[SK]: SK_CONST
|
|
305
|
+
});
|
|
306
|
+
var lookupPkValue = (model, index, values) => partitionKey(
|
|
307
|
+
`${LOOKUP_PREFIX}#${component(model)}#${component(index)}#${joinValues(values)}`,
|
|
308
|
+
"lookup partition key"
|
|
309
|
+
);
|
|
181
310
|
function encodeKeys(model, item, indexMap, assignment) {
|
|
182
311
|
const id = String(item.id);
|
|
183
312
|
const keys = {
|
|
184
313
|
...primaryKey(model, id),
|
|
185
314
|
[TYPE_PK]: model,
|
|
186
|
-
|
|
315
|
+
// Deliberately *not* length-prefixed: this sort key exists to order a
|
|
316
|
+
// model's rows by creation time, and a byte-length prefix would sort
|
|
317
|
+
// lexicographically by length instead of chronologically. It is never used
|
|
318
|
+
// for `begins_with` lookups, so it carries no collision risk.
|
|
319
|
+
[TYPE_SK]: sortKey(
|
|
320
|
+
`${String(item.createdAt ?? "")}#${id}`,
|
|
321
|
+
"type sort key"
|
|
322
|
+
)
|
|
187
323
|
};
|
|
188
324
|
for (const pattern of indexMap[model] ?? []) {
|
|
189
325
|
const pkValues = pattern.pk.map((f) => item[f]);
|
|
@@ -192,14 +328,17 @@ function encodeKeys(model, item, indexMap, assignment) {
|
|
|
192
328
|
if (!slot) continue;
|
|
193
329
|
keys[gsiPk(slot)] = lookupPkValue(model, pattern.index, pkValues);
|
|
194
330
|
const skValues = (pattern.sk ?? []).map((f) => item[f]);
|
|
195
|
-
keys[gsiSk(slot)] =
|
|
331
|
+
keys[gsiSk(slot)] = sortKey(
|
|
332
|
+
skValues.some((v) => v == null) ? component(id) : `${joinValues(skValues)}#${component(id)}`,
|
|
333
|
+
"lookup sort key"
|
|
334
|
+
);
|
|
196
335
|
}
|
|
197
336
|
return keys;
|
|
198
337
|
}
|
|
199
338
|
function encodeLookupQuery(model, index, key, indexMap, assignment) {
|
|
200
339
|
const slot = assignment.slots[model]?.[index];
|
|
201
340
|
if (!slot) {
|
|
202
|
-
throw new
|
|
341
|
+
throw new DynamoDBAdapterError(
|
|
203
342
|
`No physical slot for index "${index}" on model "${model}". Ensure the index is present in the adapter's index map.`
|
|
204
343
|
);
|
|
205
344
|
}
|
|
@@ -227,6 +366,7 @@ function stripReserved(item) {
|
|
|
227
366
|
import {
|
|
228
367
|
CreateTableCommand,
|
|
229
368
|
ResourceInUseException,
|
|
369
|
+
UpdateTimeToLiveCommand,
|
|
230
370
|
waitUntilTableExists
|
|
231
371
|
} from "@aws-sdk/client-dynamodb";
|
|
232
372
|
function buildTableDefinition(tableName, lookupSlots) {
|
|
@@ -283,9 +423,32 @@ async function ensureSchema(opts) {
|
|
|
283
423
|
{ client: opts.client, maxWaitTime: 60 },
|
|
284
424
|
{ TableName: opts.tableName }
|
|
285
425
|
);
|
|
426
|
+
if (!opts.ttlAttribute) return;
|
|
427
|
+
try {
|
|
428
|
+
await opts.client.send(
|
|
429
|
+
new UpdateTimeToLiveCommand({
|
|
430
|
+
TableName: opts.tableName,
|
|
431
|
+
TimeToLiveSpecification: {
|
|
432
|
+
Enabled: true,
|
|
433
|
+
AttributeName: opts.ttlAttribute
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (!isAlreadyEnabled(error)) throw error;
|
|
439
|
+
}
|
|
286
440
|
}
|
|
441
|
+
var isAlreadyEnabled = (error) => typeof error === "object" && error !== null && "name" in error && error.name === "ValidationException" && typeof error.message === "string" && error.message.includes("already");
|
|
287
442
|
function generateSchemaFile(opts) {
|
|
288
|
-
const table =
|
|
443
|
+
const table = {
|
|
444
|
+
...buildTableDefinition(opts.tableName, opts.lookupSlots),
|
|
445
|
+
...opts.ttlAttribute ? {
|
|
446
|
+
TimeToLiveSpecification: {
|
|
447
|
+
Enabled: true,
|
|
448
|
+
AttributeName: opts.ttlAttribute
|
|
449
|
+
}
|
|
450
|
+
} : {}
|
|
451
|
+
};
|
|
289
452
|
const template = {
|
|
290
453
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
291
454
|
Resources: {
|
|
@@ -309,6 +472,14 @@ export default dynamoDBSchema;
|
|
|
309
472
|
}
|
|
310
473
|
|
|
311
474
|
// src/stores/default/single-table-store.ts
|
|
475
|
+
var MAX_TRANSACT_ITEMS = 100;
|
|
476
|
+
var MAX_WRITE_ATTEMPTS = 3;
|
|
477
|
+
var DEFAULT_MAX_PAGES2 = 25;
|
|
478
|
+
var epochSeconds = (value) => {
|
|
479
|
+
const date = value instanceof Date ? value : typeof value === "string" || typeof value === "number" ? new Date(value) : void 0;
|
|
480
|
+
if (!date || Number.isNaN(date.getTime())) return void 0;
|
|
481
|
+
return Math.floor(date.getTime() / 1e3);
|
|
482
|
+
};
|
|
312
483
|
function createSingleTableStore(opts) {
|
|
313
484
|
const tableName = opts.tableName ?? process.env.DYNAMODB_TABLE_NAME ?? "better-auth";
|
|
314
485
|
const doc = opts.documentClient ?? DynamoDBDocumentClient.from(
|
|
@@ -320,107 +491,400 @@ function createSingleTableStore(opts) {
|
|
|
320
491
|
);
|
|
321
492
|
const indexMap = opts.indexMap;
|
|
322
493
|
const assignment = assignSlots(indexMap);
|
|
494
|
+
const atomicUniqueness = opts.atomicUniqueness ?? true;
|
|
495
|
+
const maxPages = opts.maxPages ?? DEFAULT_MAX_PAGES2;
|
|
496
|
+
const ttl = opts.ttl === false ? void 0 : opts.ttl;
|
|
497
|
+
const ttlAttribute = ttl?.attributeName ?? DEFAULT_TTL_ATTRIBUTE;
|
|
498
|
+
const ttlFieldFor = (model) => ttl ? ttl.fields?.[model] ?? ttl.defaultField : void 0;
|
|
499
|
+
const ttlAttributesFor = (model, item) => {
|
|
500
|
+
const field = ttlFieldFor(model);
|
|
501
|
+
if (!field) return {};
|
|
502
|
+
const seconds = epochSeconds(item[field]);
|
|
503
|
+
return seconds === void 0 ? {} : { [ttlAttribute]: seconds };
|
|
504
|
+
};
|
|
505
|
+
const isExpired = (item) => {
|
|
506
|
+
if (!item || !ttl) return false;
|
|
507
|
+
const seconds = item[ttlAttribute];
|
|
508
|
+
return typeof seconds === "number" && seconds <= Math.floor(Date.now() / 1e3);
|
|
509
|
+
};
|
|
510
|
+
const modelHasTtl = (model) => Boolean(ttlFieldFor(model));
|
|
511
|
+
const uniquePatternsFor = (model) => atomicUniqueness ? (indexMap[model] ?? []).filter((p) => p.unique) : [];
|
|
512
|
+
const uniqueFieldNames = (model) => uniquePatternsFor(model).flatMap((p) => p.pk);
|
|
513
|
+
const markerItem = (model, pattern, values, ownerId, source) => ({
|
|
514
|
+
...uniqueMarkerKey(model, pattern.index, values),
|
|
515
|
+
__ba_owner: ownerId,
|
|
516
|
+
...ttlAttributesFor(model, source)
|
|
517
|
+
});
|
|
518
|
+
const applicableValues = (pattern, item) => {
|
|
519
|
+
const values = pattern.pk.map((f) => item[f]);
|
|
520
|
+
return values.some((v) => v == null) ? null : values;
|
|
521
|
+
};
|
|
522
|
+
const uniqueMarkerPuts = (model, item, id) => uniquePatternsFor(model).flatMap((pattern) => {
|
|
523
|
+
const values = applicableValues(pattern, item);
|
|
524
|
+
if (!values) return [];
|
|
525
|
+
return [
|
|
526
|
+
{
|
|
527
|
+
Put: {
|
|
528
|
+
TableName: tableName,
|
|
529
|
+
Item: markerItem(model, pattern, values, id, item),
|
|
530
|
+
ConditionExpression: "attribute_not_exists(#pk)",
|
|
531
|
+
ExpressionAttributeNames: { "#pk": PK }
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
];
|
|
535
|
+
});
|
|
536
|
+
const uniqueMarkerDeletes = (model, item) => {
|
|
537
|
+
if (!item) return [];
|
|
538
|
+
return uniquePatternsFor(model).flatMap((pattern) => {
|
|
539
|
+
const values = applicableValues(pattern, item);
|
|
540
|
+
if (!values) return [];
|
|
541
|
+
return [
|
|
542
|
+
{
|
|
543
|
+
Delete: {
|
|
544
|
+
TableName: tableName,
|
|
545
|
+
Key: uniqueMarkerKey(model, pattern.index, values)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
];
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
const uniqueMarkerDiff = (model, before, after, id) => {
|
|
552
|
+
const items = [];
|
|
553
|
+
for (const pattern of uniquePatternsFor(model)) {
|
|
554
|
+
const oldValues = applicableValues(pattern, before);
|
|
555
|
+
const newValues = applicableValues(pattern, after);
|
|
556
|
+
if (oldValues?.length === newValues?.length && oldValues?.every((v, i) => v === newValues[i])) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (oldValues) {
|
|
560
|
+
items.push({
|
|
561
|
+
Delete: {
|
|
562
|
+
TableName: tableName,
|
|
563
|
+
Key: uniqueMarkerKey(model, pattern.index, oldValues)
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
if (newValues) {
|
|
568
|
+
items.push({
|
|
569
|
+
Put: {
|
|
570
|
+
TableName: tableName,
|
|
571
|
+
Item: markerItem(model, pattern, newValues, id, after),
|
|
572
|
+
ConditionExpression: "attribute_not_exists(#pk)",
|
|
573
|
+
ExpressionAttributeNames: { "#pk": PK }
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return items;
|
|
579
|
+
};
|
|
580
|
+
const keyOf = (action) => {
|
|
581
|
+
const target = action.Put?.Item ?? action.Delete?.Key ?? action.Update?.Key ?? {};
|
|
582
|
+
return `${String(target[PK])}`;
|
|
583
|
+
};
|
|
584
|
+
const validateTransaction = (model, items) => {
|
|
585
|
+
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
586
|
+
throw new DynamoDBAdapterError(
|
|
587
|
+
`better-auth-dynamodb: a single write to "${model}" would need ${items.length} transaction actions, over DynamoDB's ${MAX_TRANSACT_ITEMS}-action limit. Reduce the number of unique fields on this model.`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
591
|
+
for (const item of items) {
|
|
592
|
+
const key = keyOf(item);
|
|
593
|
+
if (seen.has(key)) {
|
|
594
|
+
throw new DynamoDBAdapterError(
|
|
595
|
+
`better-auth-dynamodb: internal error \u2014 two actions in one transaction target the same item on "${model}". Please report this.`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
seen.add(key);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const sendTransaction = async (model, items) => {
|
|
602
|
+
validateTransaction(model, items);
|
|
603
|
+
await doc.send(
|
|
604
|
+
new TransactWriteCommand({
|
|
605
|
+
TransactItems: items,
|
|
606
|
+
// Gives the SDK's internal retries of this one send a stable token.
|
|
607
|
+
// Not cross-invocation idempotency.
|
|
608
|
+
ClientRequestToken: randomUUID()
|
|
609
|
+
})
|
|
610
|
+
);
|
|
611
|
+
};
|
|
612
|
+
const asUniqueConflict = (model, err) => {
|
|
613
|
+
if (isConditionalTransactionCanceled(err)) {
|
|
614
|
+
throw new UniqueConstraintError(model, uniqueFieldNames(model), {
|
|
615
|
+
cause: err
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
throw err;
|
|
619
|
+
};
|
|
620
|
+
const readRaw = async (model, id) => {
|
|
621
|
+
const res = await doc.send(
|
|
622
|
+
new GetCommand({
|
|
623
|
+
TableName: tableName,
|
|
624
|
+
Key: primaryKey(model, id),
|
|
625
|
+
ConsistentRead: true
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
if (!res.Item || isExpired(res.Item)) return null;
|
|
629
|
+
return res.Item;
|
|
630
|
+
};
|
|
631
|
+
const revisionGuard = (existing) => {
|
|
632
|
+
const revision = existing[REVISION];
|
|
633
|
+
return revision === void 0 ? {
|
|
634
|
+
ConditionExpression: "attribute_exists(#pk) AND attribute_not_exists(#rev)",
|
|
635
|
+
ExpressionAttributeNames: { "#pk": PK, "#rev": REVISION }
|
|
636
|
+
} : {
|
|
637
|
+
ConditionExpression: "attribute_exists(#pk) AND #rev = :rev",
|
|
638
|
+
ExpressionAttributeNames: { "#pk": PK, "#rev": REVISION },
|
|
639
|
+
ExpressionAttributeValues: { ":rev": revision }
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
const rejectedByGuard = (err) => isConditionalCheckFailed(err) || isConditionalTransactionCanceled(err);
|
|
323
643
|
const query = async (input, cursor, countOnly = false) => {
|
|
324
644
|
const res = await doc.send(
|
|
325
645
|
new QueryCommand({
|
|
326
646
|
TableName: tableName,
|
|
327
647
|
ExclusiveStartKey: cursor,
|
|
328
648
|
...countOnly ? { Select: "COUNT" } : {},
|
|
649
|
+
...opts.pageSize ? { Limit: opts.pageSize } : {},
|
|
329
650
|
...input
|
|
330
651
|
})
|
|
331
652
|
);
|
|
332
653
|
return {
|
|
333
|
-
items: countOnly ? [] : (res.Items ?? []).map((i) => stripReserved(i)),
|
|
654
|
+
items: countOnly ? [] : (res.Items ?? []).filter((item) => !isExpired(item)).map((i) => stripReserved(i)),
|
|
334
655
|
count: res.Count ?? 0,
|
|
335
656
|
cursor: res.LastEvaluatedKey
|
|
336
657
|
};
|
|
337
658
|
};
|
|
659
|
+
const putEntity = (model, item) => ({
|
|
660
|
+
Put: {
|
|
661
|
+
TableName: tableName,
|
|
662
|
+
Item: {
|
|
663
|
+
...item,
|
|
664
|
+
...encodeKeys(model, item, indexMap, assignment),
|
|
665
|
+
...ttlAttributesFor(model, item),
|
|
666
|
+
[REVISION]: randomUUID()
|
|
667
|
+
},
|
|
668
|
+
ConditionExpression: "attribute_not_exists(#pk)",
|
|
669
|
+
ExpressionAttributeNames: { "#pk": PK }
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
const guardedWrite = async (model, id, build, onMissing) => {
|
|
673
|
+
for (let attempt = 1; attempt <= MAX_WRITE_ATTEMPTS; attempt++) {
|
|
674
|
+
const existing = await readRaw(model, id);
|
|
675
|
+
if (!existing) return onMissing();
|
|
676
|
+
const { items, result } = build(existing);
|
|
677
|
+
try {
|
|
678
|
+
await sendTransaction(model, items);
|
|
679
|
+
return result;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const guarded = rejectedByGuard(err);
|
|
682
|
+
if (!guarded && !isTransactionConflict(err)) throw err;
|
|
683
|
+
if (guarded && await lostToUniqueness(model, id, existing)) {
|
|
684
|
+
return asUniqueConflict(model, err);
|
|
685
|
+
}
|
|
686
|
+
if (attempt === MAX_WRITE_ATTEMPTS) {
|
|
687
|
+
throw new OptimisticLockError(model, id, { cause: err });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
throw new OptimisticLockError(model, id);
|
|
692
|
+
};
|
|
693
|
+
const lostToUniqueness = async (model, id, previous) => {
|
|
694
|
+
if (uniquePatternsFor(model).length === 0) return false;
|
|
695
|
+
const current = await readRaw(model, id);
|
|
696
|
+
return Boolean(current) && current[REVISION] === previous[REVISION];
|
|
697
|
+
};
|
|
338
698
|
return {
|
|
339
699
|
async put(model, item) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
700
|
+
const id = String(item.id);
|
|
701
|
+
const entity = putEntity(model, item);
|
|
702
|
+
const markers = uniqueMarkerPuts(model, item, id);
|
|
703
|
+
try {
|
|
704
|
+
if (markers.length === 0) {
|
|
705
|
+
await doc.send(new PutCommand({ ...entity.Put }));
|
|
706
|
+
} else {
|
|
707
|
+
await sendTransaction(model, [entity, ...markers]);
|
|
708
|
+
}
|
|
709
|
+
} catch (err) {
|
|
710
|
+
if (isConditionalCheckFailed(err)) {
|
|
711
|
+
throw new DynamoDBAdapterError(
|
|
712
|
+
`better-auth-dynamodb: a "${model}" row with id "${id}" already exists.`,
|
|
713
|
+
{ cause: err }
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
asUniqueConflict(model, err);
|
|
717
|
+
}
|
|
346
718
|
return item;
|
|
347
719
|
},
|
|
348
720
|
async getById(model, id) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
);
|
|
352
|
-
return stripReserved(res.Item);
|
|
721
|
+
const raw = await readRaw(model, id);
|
|
722
|
+
return stripReserved(raw);
|
|
353
723
|
},
|
|
354
724
|
async update(model, id, patch) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
725
|
+
return guardedWrite(
|
|
726
|
+
model,
|
|
727
|
+
id,
|
|
728
|
+
(existing) => {
|
|
729
|
+
const clean = stripReserved(existing);
|
|
730
|
+
const merged = { ...clean, ...patch };
|
|
731
|
+
return {
|
|
732
|
+
items: [
|
|
733
|
+
{
|
|
734
|
+
Put: {
|
|
735
|
+
TableName: tableName,
|
|
736
|
+
Item: {
|
|
737
|
+
...merged,
|
|
738
|
+
...encodeKeys(model, merged, indexMap, assignment),
|
|
739
|
+
...ttlAttributesFor(model, merged),
|
|
740
|
+
[REVISION]: randomUUID()
|
|
741
|
+
},
|
|
742
|
+
...revisionGuard(existing)
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
...uniqueMarkerDiff(model, clean, merged, id)
|
|
746
|
+
],
|
|
747
|
+
result: merged
|
|
748
|
+
};
|
|
749
|
+
},
|
|
750
|
+
() => null
|
|
369
751
|
);
|
|
370
|
-
return merged;
|
|
371
752
|
},
|
|
372
753
|
async deleteById(model, id) {
|
|
373
|
-
await
|
|
374
|
-
|
|
754
|
+
await guardedWrite(
|
|
755
|
+
model,
|
|
756
|
+
id,
|
|
757
|
+
(existing) => ({
|
|
758
|
+
items: [
|
|
759
|
+
{
|
|
760
|
+
Delete: {
|
|
761
|
+
TableName: tableName,
|
|
762
|
+
Key: primaryKey(model, id),
|
|
763
|
+
...revisionGuard(existing)
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
...uniqueMarkerDeletes(model, stripReserved(existing))
|
|
767
|
+
],
|
|
768
|
+
result: void 0
|
|
769
|
+
}),
|
|
770
|
+
() => void 0
|
|
375
771
|
);
|
|
376
772
|
},
|
|
377
773
|
async consumeOne(model, id) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
774
|
+
if (uniquePatternsFor(model).length === 0) {
|
|
775
|
+
const res = await doc.send(
|
|
776
|
+
new DeleteCommand({
|
|
777
|
+
TableName: tableName,
|
|
778
|
+
Key: primaryKey(model, id),
|
|
779
|
+
ReturnValues: "ALL_OLD"
|
|
780
|
+
})
|
|
781
|
+
);
|
|
782
|
+
const item = res.Attributes;
|
|
783
|
+
return isExpired(item) ? null : stripReserved(item);
|
|
784
|
+
}
|
|
785
|
+
const existing = await readRaw(model, id);
|
|
786
|
+
if (!existing) return null;
|
|
787
|
+
const clean = stripReserved(existing);
|
|
788
|
+
try {
|
|
789
|
+
await sendTransaction(model, [
|
|
790
|
+
{
|
|
791
|
+
Delete: {
|
|
792
|
+
TableName: tableName,
|
|
793
|
+
Key: primaryKey(model, id),
|
|
794
|
+
...revisionGuard(existing)
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
...uniqueMarkerDeletes(model, clean)
|
|
798
|
+
]);
|
|
799
|
+
} catch (err) {
|
|
800
|
+
if (rejectedByGuard(err)) return null;
|
|
801
|
+
throw err;
|
|
802
|
+
}
|
|
803
|
+
return clean;
|
|
386
804
|
},
|
|
387
805
|
async incrementOne(model, id, { increment, set }) {
|
|
388
|
-
const
|
|
389
|
-
|
|
806
|
+
const indexedFields = new Set(
|
|
807
|
+
(indexMap[model] ?? []).flatMap((p) => [...p.pk, ...p.sk ?? []])
|
|
808
|
+
);
|
|
809
|
+
const touchesIndex = Object.keys(set ?? {}).some(
|
|
810
|
+
(f) => indexedFields.has(f)
|
|
811
|
+
);
|
|
812
|
+
const ttlField = ttlFieldFor(model);
|
|
813
|
+
const touchesTtl = Boolean(ttlField && set && ttlField in set);
|
|
814
|
+
if (touchesIndex || touchesTtl) {
|
|
815
|
+
return guardedWrite(
|
|
816
|
+
model,
|
|
817
|
+
id,
|
|
818
|
+
(existing) => {
|
|
819
|
+
const clean = stripReserved(existing);
|
|
820
|
+
const merged = { ...clean, ...set ?? {} };
|
|
821
|
+
for (const [field, delta] of Object.entries(increment)) {
|
|
822
|
+
const current = merged[field];
|
|
823
|
+
merged[field] = (typeof current === "number" ? current : 0) + delta;
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
items: [
|
|
827
|
+
{
|
|
828
|
+
Put: {
|
|
829
|
+
TableName: tableName,
|
|
830
|
+
Item: {
|
|
831
|
+
...merged,
|
|
832
|
+
...encodeKeys(model, merged, indexMap, assignment),
|
|
833
|
+
...ttlAttributesFor(model, merged),
|
|
834
|
+
[REVISION]: randomUUID()
|
|
835
|
+
},
|
|
836
|
+
...revisionGuard(existing)
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
...uniqueMarkerDiff(model, clean, merged, id)
|
|
840
|
+
],
|
|
841
|
+
result: merged
|
|
842
|
+
};
|
|
843
|
+
},
|
|
844
|
+
() => null
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const names = { "#pk": PK, "#rev": REVISION };
|
|
848
|
+
const values = { ":rev": randomUUID() };
|
|
390
849
|
const addClauses = [];
|
|
391
|
-
const setClauses = [];
|
|
850
|
+
const setClauses = ["#rev = :rev"];
|
|
392
851
|
let i = 0;
|
|
393
852
|
for (const [field, delta] of Object.entries(increment)) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
values[valueKey] = delta;
|
|
398
|
-
addClauses.push(`${nameKey} ${valueKey}`);
|
|
853
|
+
names[`#f${i}`] = field;
|
|
854
|
+
values[`:v${i}`] = delta;
|
|
855
|
+
addClauses.push(`#f${i} :v${i}`);
|
|
399
856
|
i++;
|
|
400
857
|
}
|
|
401
858
|
for (const [field, value] of Object.entries(set ?? {})) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
values[valueKey] = value;
|
|
406
|
-
setClauses.push(`${nameKey} = ${valueKey}`);
|
|
859
|
+
names[`#f${i}`] = field;
|
|
860
|
+
values[`:v${i}`] = value;
|
|
861
|
+
setClauses.push(`#f${i} = :v${i}`);
|
|
407
862
|
i++;
|
|
408
863
|
}
|
|
409
864
|
const expression = [
|
|
410
|
-
|
|
865
|
+
`SET ${setClauses.join(", ")}`,
|
|
411
866
|
addClauses.length ? `ADD ${addClauses.join(", ")}` : null
|
|
412
867
|
].filter(Boolean).join(" ");
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
868
|
+
try {
|
|
869
|
+
const res = await doc.send(
|
|
870
|
+
new UpdateCommand({
|
|
871
|
+
TableName: tableName,
|
|
872
|
+
Key: primaryKey(model, id),
|
|
873
|
+
UpdateExpression: expression,
|
|
874
|
+
// Without this, `ADD` would happily create the row it was told to
|
|
875
|
+
// increment, turning "increment an existing counter" into an upsert.
|
|
876
|
+
ConditionExpression: "attribute_exists(#pk)",
|
|
877
|
+
ExpressionAttributeNames: names,
|
|
878
|
+
ExpressionAttributeValues: values,
|
|
879
|
+
ReturnValues: "ALL_NEW"
|
|
880
|
+
})
|
|
881
|
+
);
|
|
882
|
+
const item = res.Attributes;
|
|
883
|
+
return isExpired(item) ? null : stripReserved(item);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
if (isConditionalCheckFailed(err)) return null;
|
|
886
|
+
throw err;
|
|
887
|
+
}
|
|
424
888
|
},
|
|
425
889
|
async queryIndex({ model, index, key, cursor }) {
|
|
426
890
|
const lookup = encodeLookupQuery(model, index, key, indexMap, assignment);
|
|
@@ -456,6 +920,7 @@ function createSingleTableStore(opts) {
|
|
|
456
920
|
return { items, cursor: next };
|
|
457
921
|
},
|
|
458
922
|
async count({ model, index, key }) {
|
|
923
|
+
if (modelHasTtl(model)) return null;
|
|
459
924
|
let base;
|
|
460
925
|
if (index && key) {
|
|
461
926
|
const lookup = encodeLookupQuery(
|
|
@@ -481,10 +946,16 @@ function createSingleTableStore(opts) {
|
|
|
481
946
|
}
|
|
482
947
|
let total = 0;
|
|
483
948
|
let cursor = void 0;
|
|
949
|
+
let pages = 0;
|
|
484
950
|
do {
|
|
485
951
|
const page = await query(base, cursor, true);
|
|
486
952
|
total += page.count;
|
|
487
953
|
cursor = page.cursor;
|
|
954
|
+
if (++pages >= maxPages && cursor) {
|
|
955
|
+
throw new DynamoDBAdapterError(
|
|
956
|
+
`better-auth-dynamodb: counting "${model}" exceeded maxPages (${maxPages}) with more pages remaining. Raise maxPages or narrow the query \u2014 returning a partial count would be wrong.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
488
959
|
} while (cursor);
|
|
489
960
|
return total;
|
|
490
961
|
},
|
|
@@ -492,7 +963,8 @@ function createSingleTableStore(opts) {
|
|
|
492
963
|
generateSchemaFile({
|
|
493
964
|
tableName,
|
|
494
965
|
lookupSlots: assignment.maxSlots,
|
|
495
|
-
file
|
|
966
|
+
file,
|
|
967
|
+
ttlAttribute: ttl ? ttlAttribute : void 0
|
|
496
968
|
})
|
|
497
969
|
)
|
|
498
970
|
};
|
|
@@ -517,12 +989,18 @@ var dynamoAdapter = (config = {}) => {
|
|
|
517
989
|
customTransformInput: ({ data, fieldAttributes }) => fieldAttributes.type === "date" && data instanceof Date ? data.toISOString() : data,
|
|
518
990
|
customTransformOutput: ({ data, fieldAttributes }) => fieldAttributes.type === "date" && typeof data === "string" ? new Date(data) : data
|
|
519
991
|
},
|
|
520
|
-
adapter: ({ schema, debugLog, getDefaultModelName }) => {
|
|
992
|
+
adapter: ({ schema, debugLog, getDefaultModelName, getFieldName }) => {
|
|
521
993
|
const indexMap = config.indexMap ?? deriveIndexMap(schema);
|
|
994
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
522
995
|
const store = config.store ?? createSingleTableStore({
|
|
523
996
|
tableName: config.tableName,
|
|
524
997
|
region: config.region,
|
|
525
998
|
endpoint: config.endpoint,
|
|
999
|
+
documentClient: config.documentClient,
|
|
1000
|
+
atomicUniqueness: config.atomicUniqueness,
|
|
1001
|
+
maxPages,
|
|
1002
|
+
pageSize: config.pageSize,
|
|
1003
|
+
ttl: config.ttl,
|
|
526
1004
|
indexMap
|
|
527
1005
|
});
|
|
528
1006
|
const queryAll = async (model, where) => {
|
|
@@ -533,6 +1011,12 @@ var dynamoAdapter = (config = {}) => {
|
|
|
533
1011
|
if (plan.kind === "byId") {
|
|
534
1012
|
const one = await store.getById(model, plan.id);
|
|
535
1013
|
candidates = one ? [one] : [];
|
|
1014
|
+
} else if (plan.kind === "byIds") {
|
|
1015
|
+
const found = await mapBatched(
|
|
1016
|
+
plan.ids,
|
|
1017
|
+
(id) => store.getById(model, id)
|
|
1018
|
+
);
|
|
1019
|
+
candidates = found.filter((item) => item !== null);
|
|
536
1020
|
} else if (plan.kind === "index") {
|
|
537
1021
|
candidates = await drainPages(
|
|
538
1022
|
(cursor) => store.queryIndex({
|
|
@@ -540,11 +1024,20 @@ var dynamoAdapter = (config = {}) => {
|
|
|
540
1024
|
index: plan.index,
|
|
541
1025
|
key: plan.key,
|
|
542
1026
|
cursor
|
|
543
|
-
})
|
|
1027
|
+
}),
|
|
1028
|
+
maxPages,
|
|
1029
|
+
`index query on "${model}"`
|
|
544
1030
|
);
|
|
545
1031
|
} else {
|
|
1032
|
+
if (!config.unsafeAllowScan) {
|
|
1033
|
+
throw new UnsupportedQueryError(
|
|
1034
|
+
`better-auth-dynamodb: no index can serve this query on "${model}" (${describeWhere(clauses)}), so it would have to read every row of the model and filter in memory. Add the field to the index map \u2014 or to the Better Auth schema as \`unique\`, \`references\`, or \`index: true\` \u2014 or set \`unsafeAllowScan: true\` to accept the cost.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
546
1037
|
candidates = await drainPages(
|
|
547
|
-
(cursor) => store.listByType({ model, cursor })
|
|
1038
|
+
(cursor) => store.listByType({ model, cursor }),
|
|
1039
|
+
maxPages,
|
|
1040
|
+
`model scan of "${model}"`
|
|
548
1041
|
);
|
|
549
1042
|
}
|
|
550
1043
|
return candidates.filter(
|
|
@@ -553,10 +1046,21 @@ var dynamoAdapter = (config = {}) => {
|
|
|
553
1046
|
};
|
|
554
1047
|
const resolveId = async (model, where) => {
|
|
555
1048
|
const plan = planQuery(model, where, indexMap);
|
|
556
|
-
if (plan.kind === "byId") return plan.id;
|
|
1049
|
+
if (plan.kind === "byId" && plan.residual.length === 0) return plan.id;
|
|
557
1050
|
const [first] = await queryAll(model, where);
|
|
558
1051
|
return first ? String(first.id) : null;
|
|
559
1052
|
};
|
|
1053
|
+
const project = (model, items, select) => {
|
|
1054
|
+
if (!select?.length) return items;
|
|
1055
|
+
const stored = select.map((field) => getFieldName({ model, field }));
|
|
1056
|
+
return items.map((item) => {
|
|
1057
|
+
const picked = {};
|
|
1058
|
+
for (const field of stored) {
|
|
1059
|
+
if (field in item) picked[field] = item[field];
|
|
1060
|
+
}
|
|
1061
|
+
return picked;
|
|
1062
|
+
});
|
|
1063
|
+
};
|
|
560
1064
|
return {
|
|
561
1065
|
async create({
|
|
562
1066
|
model,
|
|
@@ -612,10 +1116,11 @@ var dynamoAdapter = (config = {}) => {
|
|
|
612
1116
|
},
|
|
613
1117
|
async findOne({
|
|
614
1118
|
model,
|
|
615
|
-
where
|
|
1119
|
+
where,
|
|
1120
|
+
select
|
|
616
1121
|
}) {
|
|
617
1122
|
const m = getDefaultModelName(model);
|
|
618
|
-
const [first] = await queryAll(m, where);
|
|
1123
|
+
const [first] = project(m, await queryAll(m, where), select);
|
|
619
1124
|
return first ?? null;
|
|
620
1125
|
},
|
|
621
1126
|
async findMany({
|
|
@@ -623,13 +1128,18 @@ var dynamoAdapter = (config = {}) => {
|
|
|
623
1128
|
where,
|
|
624
1129
|
limit,
|
|
625
1130
|
sortBy,
|
|
626
|
-
offset
|
|
1131
|
+
offset,
|
|
1132
|
+
select
|
|
627
1133
|
}) {
|
|
628
1134
|
const m = getDefaultModelName(model);
|
|
629
|
-
const items =
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1135
|
+
const items = project(
|
|
1136
|
+
m,
|
|
1137
|
+
applyWindow(
|
|
1138
|
+
applySort(await queryAll(m, where), sortBy),
|
|
1139
|
+
offset,
|
|
1140
|
+
limit
|
|
1141
|
+
),
|
|
1142
|
+
select
|
|
633
1143
|
);
|
|
634
1144
|
return items;
|
|
635
1145
|
},
|
|
@@ -641,10 +1151,17 @@ var dynamoAdapter = (config = {}) => {
|
|
|
641
1151
|
const clauses = where ?? [];
|
|
642
1152
|
const plan = planQuery(m, clauses, indexMap);
|
|
643
1153
|
if (plan.residual.length === 0 && store.count) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1154
|
+
if (plan.kind === "index") {
|
|
1155
|
+
const fast = await store.count({
|
|
1156
|
+
model: m,
|
|
1157
|
+
index: plan.index,
|
|
1158
|
+
key: plan.key
|
|
1159
|
+
});
|
|
1160
|
+
if (fast != null) return fast;
|
|
1161
|
+
} else if (plan.kind === "listByType") {
|
|
1162
|
+
const fast = await store.count({ model: m });
|
|
1163
|
+
if (fast != null) return fast;
|
|
1164
|
+
}
|
|
648
1165
|
}
|
|
649
1166
|
return (await queryAll(m, clauses)).length;
|
|
650
1167
|
},
|
|
@@ -655,7 +1172,8 @@ var dynamoAdapter = (config = {}) => {
|
|
|
655
1172
|
const m = getDefaultModelName(model);
|
|
656
1173
|
const id = await resolveId(m, where);
|
|
657
1174
|
if (!id) return null;
|
|
658
|
-
if (store.consumeOne)
|
|
1175
|
+
if (store.consumeOne)
|
|
1176
|
+
return await store.consumeOne(m, id);
|
|
659
1177
|
const item = await store.getById(m, id);
|
|
660
1178
|
if (!item) return null;
|
|
661
1179
|
await store.deleteById(m, id);
|
|
@@ -671,7 +1189,10 @@ var dynamoAdapter = (config = {}) => {
|
|
|
671
1189
|
const id = await resolveId(m, where);
|
|
672
1190
|
if (!id) return null;
|
|
673
1191
|
if (store.incrementOne) {
|
|
674
|
-
return await store.incrementOne(m, id, {
|
|
1192
|
+
return await store.incrementOne(m, id, {
|
|
1193
|
+
increment,
|
|
1194
|
+
set
|
|
1195
|
+
});
|
|
675
1196
|
}
|
|
676
1197
|
const current = await store.getById(m, id);
|
|
677
1198
|
if (!current) return null;
|
|
@@ -688,12 +1209,28 @@ var dynamoAdapter = (config = {}) => {
|
|
|
688
1209
|
}
|
|
689
1210
|
});
|
|
690
1211
|
};
|
|
1212
|
+
function describeWhere(where) {
|
|
1213
|
+
if (where.length === 0) return "no where clause";
|
|
1214
|
+
return where.map((w) => `${w.field} ${w.operator}`).join(", ");
|
|
1215
|
+
}
|
|
1216
|
+
async function mapBatched(items, op, chunkSize = 10) {
|
|
1217
|
+
const out = [];
|
|
1218
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
1219
|
+
out.push(...await Promise.all(items.slice(i, i + chunkSize).map(op)));
|
|
1220
|
+
}
|
|
1221
|
+
return out;
|
|
1222
|
+
}
|
|
691
1223
|
async function runBatched(items, op, chunkSize = 10) {
|
|
692
1224
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
693
1225
|
await Promise.all(items.slice(i, i + chunkSize).map(op));
|
|
694
1226
|
}
|
|
695
1227
|
}
|
|
696
1228
|
export {
|
|
1229
|
+
DEFAULT_TTL_ATTRIBUTE,
|
|
1230
|
+
DynamoDBAdapterError,
|
|
1231
|
+
OptimisticLockError,
|
|
1232
|
+
UniqueConstraintError,
|
|
1233
|
+
UnsupportedQueryError,
|
|
697
1234
|
assignSlots,
|
|
698
1235
|
buildTableDefinition,
|
|
699
1236
|
createSingleTableStore,
|
|
@@ -701,7 +1238,11 @@ export {
|
|
|
701
1238
|
dynamoAdapter,
|
|
702
1239
|
ensureSchema,
|
|
703
1240
|
generateSchemaFile,
|
|
1241
|
+
isConditionalCheckFailed,
|
|
1242
|
+
isConditionalTransactionCanceled,
|
|
1243
|
+
isTransactionCanceled,
|
|
704
1244
|
matchesResidual,
|
|
705
|
-
planQuery
|
|
1245
|
+
planQuery,
|
|
1246
|
+
transactionCancellationCodes
|
|
706
1247
|
};
|
|
707
1248
|
//# sourceMappingURL=index.js.map
|