@datar-platform/better-auth-dynamodb 0.1.0 → 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 +122 -0
- package/README.md +75 -10
- package/dist/index.d.ts +176 -3
- package/dist/index.js +684 -61
- 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,28 +206,76 @@ 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,
|
|
143
213
|
DynamoDBDocumentClient,
|
|
144
214
|
GetCommand,
|
|
145
215
|
PutCommand,
|
|
146
|
-
QueryCommand
|
|
216
|
+
QueryCommand,
|
|
217
|
+
TransactWriteCommand,
|
|
218
|
+
UpdateCommand
|
|
147
219
|
} from "@aws-sdk/lib-dynamodb";
|
|
148
220
|
|
|
149
221
|
// src/stores/default/key-codec.ts
|
|
222
|
+
import { createHash } from "crypto";
|
|
150
223
|
var PK = "__ba_pk";
|
|
151
224
|
var SK = "__ba_sk";
|
|
152
225
|
var TYPE_PK = "__ba_tpk";
|
|
153
226
|
var TYPE_SK = "__ba_tsk";
|
|
154
227
|
var TYPE_INDEX = "byType";
|
|
228
|
+
var REVISION = "__ba_rev";
|
|
229
|
+
var DEFAULT_TTL_ATTRIBUTE = "__ba_ttl";
|
|
155
230
|
var SK_CONST = "#";
|
|
156
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;
|
|
157
238
|
var gsiName = (slot) => `lookup${slot}`;
|
|
158
239
|
var gsiPk = (slot) => `__ba_g${slot}pk`;
|
|
159
240
|
var gsiSk = (slot) => `__ba_g${slot}sk`;
|
|
160
|
-
var
|
|
161
|
-
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");
|
|
162
279
|
function assignSlots(indexMap) {
|
|
163
280
|
const slots = {};
|
|
164
281
|
let maxSlots = 0;
|
|
@@ -173,16 +290,36 @@ function assignSlots(indexMap) {
|
|
|
173
290
|
return { slots, maxSlots };
|
|
174
291
|
}
|
|
175
292
|
var primaryKey = (model, id) => ({
|
|
176
|
-
[PK]:
|
|
293
|
+
[PK]: partitionKey(
|
|
294
|
+
`${ENTITY_PREFIX}#${component(model)}#${component(id)}`,
|
|
295
|
+
"entity partition key"
|
|
296
|
+
),
|
|
297
|
+
[SK]: SK_CONST
|
|
298
|
+
});
|
|
299
|
+
var uniqueMarkerKey = (model, index, values) => ({
|
|
300
|
+
[PK]: partitionKey(
|
|
301
|
+
`${UNIQUE_PREFIX}#${component(model)}#${component(index)}#${joinValues(values)}`,
|
|
302
|
+
"unique marker partition key"
|
|
303
|
+
),
|
|
177
304
|
[SK]: SK_CONST
|
|
178
305
|
});
|
|
179
|
-
var lookupPkValue = (model, index, values) =>
|
|
306
|
+
var lookupPkValue = (model, index, values) => partitionKey(
|
|
307
|
+
`${LOOKUP_PREFIX}#${component(model)}#${component(index)}#${joinValues(values)}`,
|
|
308
|
+
"lookup partition key"
|
|
309
|
+
);
|
|
180
310
|
function encodeKeys(model, item, indexMap, assignment) {
|
|
181
311
|
const id = String(item.id);
|
|
182
312
|
const keys = {
|
|
183
313
|
...primaryKey(model, id),
|
|
184
314
|
[TYPE_PK]: model,
|
|
185
|
-
|
|
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
|
+
)
|
|
186
323
|
};
|
|
187
324
|
for (const pattern of indexMap[model] ?? []) {
|
|
188
325
|
const pkValues = pattern.pk.map((f) => item[f]);
|
|
@@ -191,14 +328,17 @@ function encodeKeys(model, item, indexMap, assignment) {
|
|
|
191
328
|
if (!slot) continue;
|
|
192
329
|
keys[gsiPk(slot)] = lookupPkValue(model, pattern.index, pkValues);
|
|
193
330
|
const skValues = (pattern.sk ?? []).map((f) => item[f]);
|
|
194
|
-
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
|
+
);
|
|
195
335
|
}
|
|
196
336
|
return keys;
|
|
197
337
|
}
|
|
198
338
|
function encodeLookupQuery(model, index, key, indexMap, assignment) {
|
|
199
339
|
const slot = assignment.slots[model]?.[index];
|
|
200
340
|
if (!slot) {
|
|
201
|
-
throw new
|
|
341
|
+
throw new DynamoDBAdapterError(
|
|
202
342
|
`No physical slot for index "${index}" on model "${model}". Ensure the index is present in the adapter's index map.`
|
|
203
343
|
);
|
|
204
344
|
}
|
|
@@ -226,6 +366,7 @@ function stripReserved(item) {
|
|
|
226
366
|
import {
|
|
227
367
|
CreateTableCommand,
|
|
228
368
|
ResourceInUseException,
|
|
369
|
+
UpdateTimeToLiveCommand,
|
|
229
370
|
waitUntilTableExists
|
|
230
371
|
} from "@aws-sdk/client-dynamodb";
|
|
231
372
|
function buildTableDefinition(tableName, lookupSlots) {
|
|
@@ -282,9 +423,32 @@ async function ensureSchema(opts) {
|
|
|
282
423
|
{ client: opts.client, maxWaitTime: 60 },
|
|
283
424
|
{ TableName: opts.tableName }
|
|
284
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
|
+
}
|
|
285
440
|
}
|
|
441
|
+
var isAlreadyEnabled = (error) => typeof error === "object" && error !== null && "name" in error && error.name === "ValidationException" && typeof error.message === "string" && error.message.includes("already");
|
|
286
442
|
function generateSchemaFile(opts) {
|
|
287
|
-
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
|
+
};
|
|
288
452
|
const template = {
|
|
289
453
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
290
454
|
Resources: {
|
|
@@ -308,6 +472,14 @@ export default dynamoDBSchema;
|
|
|
308
472
|
}
|
|
309
473
|
|
|
310
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
|
+
};
|
|
311
483
|
function createSingleTableStore(opts) {
|
|
312
484
|
const tableName = opts.tableName ?? process.env.DYNAMODB_TABLE_NAME ?? "better-auth";
|
|
313
485
|
const doc = opts.documentClient ?? DynamoDBDocumentClient.from(
|
|
@@ -319,59 +491,400 @@ function createSingleTableStore(opts) {
|
|
|
319
491
|
);
|
|
320
492
|
const indexMap = opts.indexMap;
|
|
321
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);
|
|
322
643
|
const query = async (input, cursor, countOnly = false) => {
|
|
323
644
|
const res = await doc.send(
|
|
324
645
|
new QueryCommand({
|
|
325
646
|
TableName: tableName,
|
|
326
647
|
ExclusiveStartKey: cursor,
|
|
327
648
|
...countOnly ? { Select: "COUNT" } : {},
|
|
649
|
+
...opts.pageSize ? { Limit: opts.pageSize } : {},
|
|
328
650
|
...input
|
|
329
651
|
})
|
|
330
652
|
);
|
|
331
653
|
return {
|
|
332
|
-
items: countOnly ? [] : (res.Items ?? []).map((i) => stripReserved(i)),
|
|
654
|
+
items: countOnly ? [] : (res.Items ?? []).filter((item) => !isExpired(item)).map((i) => stripReserved(i)),
|
|
333
655
|
count: res.Count ?? 0,
|
|
334
656
|
cursor: res.LastEvaluatedKey
|
|
335
657
|
};
|
|
336
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
|
+
};
|
|
337
698
|
return {
|
|
338
699
|
async put(model, item) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
}
|
|
345
718
|
return item;
|
|
346
719
|
},
|
|
347
720
|
async getById(model, id) {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
);
|
|
351
|
-
return stripReserved(res.Item);
|
|
721
|
+
const raw = await readRaw(model, id);
|
|
722
|
+
return stripReserved(raw);
|
|
352
723
|
},
|
|
353
724
|
async update(model, id, patch) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
368
751
|
);
|
|
369
|
-
return merged;
|
|
370
752
|
},
|
|
371
753
|
async deleteById(model, id) {
|
|
372
|
-
await
|
|
373
|
-
|
|
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
|
|
771
|
+
);
|
|
772
|
+
},
|
|
773
|
+
async consumeOne(model, id) {
|
|
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;
|
|
804
|
+
},
|
|
805
|
+
async incrementOne(model, id, { increment, set }) {
|
|
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)
|
|
374
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() };
|
|
849
|
+
const addClauses = [];
|
|
850
|
+
const setClauses = ["#rev = :rev"];
|
|
851
|
+
let i = 0;
|
|
852
|
+
for (const [field, delta] of Object.entries(increment)) {
|
|
853
|
+
names[`#f${i}`] = field;
|
|
854
|
+
values[`:v${i}`] = delta;
|
|
855
|
+
addClauses.push(`#f${i} :v${i}`);
|
|
856
|
+
i++;
|
|
857
|
+
}
|
|
858
|
+
for (const [field, value] of Object.entries(set ?? {})) {
|
|
859
|
+
names[`#f${i}`] = field;
|
|
860
|
+
values[`:v${i}`] = value;
|
|
861
|
+
setClauses.push(`#f${i} = :v${i}`);
|
|
862
|
+
i++;
|
|
863
|
+
}
|
|
864
|
+
const expression = [
|
|
865
|
+
`SET ${setClauses.join(", ")}`,
|
|
866
|
+
addClauses.length ? `ADD ${addClauses.join(", ")}` : null
|
|
867
|
+
].filter(Boolean).join(" ");
|
|
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
|
+
}
|
|
375
888
|
},
|
|
376
889
|
async queryIndex({ model, index, key, cursor }) {
|
|
377
890
|
const lookup = encodeLookupQuery(model, index, key, indexMap, assignment);
|
|
@@ -407,6 +920,7 @@ function createSingleTableStore(opts) {
|
|
|
407
920
|
return { items, cursor: next };
|
|
408
921
|
},
|
|
409
922
|
async count({ model, index, key }) {
|
|
923
|
+
if (modelHasTtl(model)) return null;
|
|
410
924
|
let base;
|
|
411
925
|
if (index && key) {
|
|
412
926
|
const lookup = encodeLookupQuery(
|
|
@@ -432,10 +946,16 @@ function createSingleTableStore(opts) {
|
|
|
432
946
|
}
|
|
433
947
|
let total = 0;
|
|
434
948
|
let cursor = void 0;
|
|
949
|
+
let pages = 0;
|
|
435
950
|
do {
|
|
436
951
|
const page = await query(base, cursor, true);
|
|
437
952
|
total += page.count;
|
|
438
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
|
+
}
|
|
439
959
|
} while (cursor);
|
|
440
960
|
return total;
|
|
441
961
|
},
|
|
@@ -443,7 +963,8 @@ function createSingleTableStore(opts) {
|
|
|
443
963
|
generateSchemaFile({
|
|
444
964
|
tableName,
|
|
445
965
|
lookupSlots: assignment.maxSlots,
|
|
446
|
-
file
|
|
966
|
+
file,
|
|
967
|
+
ttlAttribute: ttl ? ttlAttribute : void 0
|
|
447
968
|
})
|
|
448
969
|
)
|
|
449
970
|
};
|
|
@@ -468,12 +989,18 @@ var dynamoAdapter = (config = {}) => {
|
|
|
468
989
|
customTransformInput: ({ data, fieldAttributes }) => fieldAttributes.type === "date" && data instanceof Date ? data.toISOString() : data,
|
|
469
990
|
customTransformOutput: ({ data, fieldAttributes }) => fieldAttributes.type === "date" && typeof data === "string" ? new Date(data) : data
|
|
470
991
|
},
|
|
471
|
-
adapter: ({ schema, debugLog, getDefaultModelName }) => {
|
|
992
|
+
adapter: ({ schema, debugLog, getDefaultModelName, getFieldName }) => {
|
|
472
993
|
const indexMap = config.indexMap ?? deriveIndexMap(schema);
|
|
994
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
473
995
|
const store = config.store ?? createSingleTableStore({
|
|
474
996
|
tableName: config.tableName,
|
|
475
997
|
region: config.region,
|
|
476
998
|
endpoint: config.endpoint,
|
|
999
|
+
documentClient: config.documentClient,
|
|
1000
|
+
atomicUniqueness: config.atomicUniqueness,
|
|
1001
|
+
maxPages,
|
|
1002
|
+
pageSize: config.pageSize,
|
|
1003
|
+
ttl: config.ttl,
|
|
477
1004
|
indexMap
|
|
478
1005
|
});
|
|
479
1006
|
const queryAll = async (model, where) => {
|
|
@@ -484,6 +1011,12 @@ var dynamoAdapter = (config = {}) => {
|
|
|
484
1011
|
if (plan.kind === "byId") {
|
|
485
1012
|
const one = await store.getById(model, plan.id);
|
|
486
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);
|
|
487
1020
|
} else if (plan.kind === "index") {
|
|
488
1021
|
candidates = await drainPages(
|
|
489
1022
|
(cursor) => store.queryIndex({
|
|
@@ -491,11 +1024,20 @@ var dynamoAdapter = (config = {}) => {
|
|
|
491
1024
|
index: plan.index,
|
|
492
1025
|
key: plan.key,
|
|
493
1026
|
cursor
|
|
494
|
-
})
|
|
1027
|
+
}),
|
|
1028
|
+
maxPages,
|
|
1029
|
+
`index query on "${model}"`
|
|
495
1030
|
);
|
|
496
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
|
+
}
|
|
497
1037
|
candidates = await drainPages(
|
|
498
|
-
(cursor) => store.listByType({ model, cursor })
|
|
1038
|
+
(cursor) => store.listByType({ model, cursor }),
|
|
1039
|
+
maxPages,
|
|
1040
|
+
`model scan of "${model}"`
|
|
499
1041
|
);
|
|
500
1042
|
}
|
|
501
1043
|
return candidates.filter(
|
|
@@ -504,10 +1046,21 @@ var dynamoAdapter = (config = {}) => {
|
|
|
504
1046
|
};
|
|
505
1047
|
const resolveId = async (model, where) => {
|
|
506
1048
|
const plan = planQuery(model, where, indexMap);
|
|
507
|
-
if (plan.kind === "byId") return plan.id;
|
|
1049
|
+
if (plan.kind === "byId" && plan.residual.length === 0) return plan.id;
|
|
508
1050
|
const [first] = await queryAll(model, where);
|
|
509
1051
|
return first ? String(first.id) : null;
|
|
510
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
|
+
};
|
|
511
1064
|
return {
|
|
512
1065
|
async create({
|
|
513
1066
|
model,
|
|
@@ -563,10 +1116,11 @@ var dynamoAdapter = (config = {}) => {
|
|
|
563
1116
|
},
|
|
564
1117
|
async findOne({
|
|
565
1118
|
model,
|
|
566
|
-
where
|
|
1119
|
+
where,
|
|
1120
|
+
select
|
|
567
1121
|
}) {
|
|
568
1122
|
const m = getDefaultModelName(model);
|
|
569
|
-
const [first] = await queryAll(m, where);
|
|
1123
|
+
const [first] = project(m, await queryAll(m, where), select);
|
|
570
1124
|
return first ?? null;
|
|
571
1125
|
},
|
|
572
1126
|
async findMany({
|
|
@@ -574,13 +1128,18 @@ var dynamoAdapter = (config = {}) => {
|
|
|
574
1128
|
where,
|
|
575
1129
|
limit,
|
|
576
1130
|
sortBy,
|
|
577
|
-
offset
|
|
1131
|
+
offset,
|
|
1132
|
+
select
|
|
578
1133
|
}) {
|
|
579
1134
|
const m = getDefaultModelName(model);
|
|
580
|
-
const items =
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1135
|
+
const items = project(
|
|
1136
|
+
m,
|
|
1137
|
+
applyWindow(
|
|
1138
|
+
applySort(await queryAll(m, where), sortBy),
|
|
1139
|
+
offset,
|
|
1140
|
+
limit
|
|
1141
|
+
),
|
|
1142
|
+
select
|
|
584
1143
|
);
|
|
585
1144
|
return items;
|
|
586
1145
|
},
|
|
@@ -592,13 +1151,57 @@ var dynamoAdapter = (config = {}) => {
|
|
|
592
1151
|
const clauses = where ?? [];
|
|
593
1152
|
const plan = planQuery(m, clauses, indexMap);
|
|
594
1153
|
if (plan.residual.length === 0 && store.count) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
}
|
|
599
1165
|
}
|
|
600
1166
|
return (await queryAll(m, clauses)).length;
|
|
601
1167
|
},
|
|
1168
|
+
async consumeOne({
|
|
1169
|
+
model,
|
|
1170
|
+
where
|
|
1171
|
+
}) {
|
|
1172
|
+
const m = getDefaultModelName(model);
|
|
1173
|
+
const id = await resolveId(m, where);
|
|
1174
|
+
if (!id) return null;
|
|
1175
|
+
if (store.consumeOne)
|
|
1176
|
+
return await store.consumeOne(m, id);
|
|
1177
|
+
const item = await store.getById(m, id);
|
|
1178
|
+
if (!item) return null;
|
|
1179
|
+
await store.deleteById(m, id);
|
|
1180
|
+
return item;
|
|
1181
|
+
},
|
|
1182
|
+
async incrementOne({
|
|
1183
|
+
model,
|
|
1184
|
+
where,
|
|
1185
|
+
increment,
|
|
1186
|
+
set
|
|
1187
|
+
}) {
|
|
1188
|
+
const m = getDefaultModelName(model);
|
|
1189
|
+
const id = await resolveId(m, where);
|
|
1190
|
+
if (!id) return null;
|
|
1191
|
+
if (store.incrementOne) {
|
|
1192
|
+
return await store.incrementOne(m, id, {
|
|
1193
|
+
increment,
|
|
1194
|
+
set
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
const current = await store.getById(m, id);
|
|
1198
|
+
if (!current) return null;
|
|
1199
|
+
const patch = { ...set };
|
|
1200
|
+
for (const [field, delta] of Object.entries(increment)) {
|
|
1201
|
+
patch[field] = (Number(current[field]) || 0) + delta;
|
|
1202
|
+
}
|
|
1203
|
+
return await store.update(m, id, patch);
|
|
1204
|
+
},
|
|
602
1205
|
...store.createSchema ? {
|
|
603
1206
|
createSchema: (props) => store.createSchema(props)
|
|
604
1207
|
} : {}
|
|
@@ -606,12 +1209,28 @@ var dynamoAdapter = (config = {}) => {
|
|
|
606
1209
|
}
|
|
607
1210
|
});
|
|
608
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
|
+
}
|
|
609
1223
|
async function runBatched(items, op, chunkSize = 10) {
|
|
610
1224
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
611
1225
|
await Promise.all(items.slice(i, i + chunkSize).map(op));
|
|
612
1226
|
}
|
|
613
1227
|
}
|
|
614
1228
|
export {
|
|
1229
|
+
DEFAULT_TTL_ATTRIBUTE,
|
|
1230
|
+
DynamoDBAdapterError,
|
|
1231
|
+
OptimisticLockError,
|
|
1232
|
+
UniqueConstraintError,
|
|
1233
|
+
UnsupportedQueryError,
|
|
615
1234
|
assignSlots,
|
|
616
1235
|
buildTableDefinition,
|
|
617
1236
|
createSingleTableStore,
|
|
@@ -619,7 +1238,11 @@ export {
|
|
|
619
1238
|
dynamoAdapter,
|
|
620
1239
|
ensureSchema,
|
|
621
1240
|
generateSchemaFile,
|
|
1241
|
+
isConditionalCheckFailed,
|
|
1242
|
+
isConditionalTransactionCanceled,
|
|
1243
|
+
isTransactionCanceled,
|
|
622
1244
|
matchesResidual,
|
|
623
|
-
planQuery
|
|
1245
|
+
planQuery,
|
|
1246
|
+
transactionCancellationCodes
|
|
624
1247
|
};
|
|
625
1248
|
//# sourceMappingURL=index.js.map
|