@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# @datar-platform/better-auth-dynamodb
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **The physical key format changed.** Every variable component of a key is now
|
|
8
|
+
written length-prefixed (`s<byteLength>:<value>`) and type-tagged, and a
|
|
9
|
+
value longer than 256 bytes is SHA-256 hashed into the key. Existing tables
|
|
10
|
+
written by 0.1.x will not be found by 0.2.0 and must be recreated. See
|
|
11
|
+
"Delimiter safety" below for why this was worth a break.
|
|
12
|
+
- **Queries no index can serve now throw** instead of silently reading every
|
|
13
|
+
row of the model and filtering in memory. Set `unsafeAllowScan: true` to
|
|
14
|
+
restore the old behaviour, or give the field an index (`unique`,
|
|
15
|
+
`references`, or `index: true` in the Better Auth schema). Native `count` is
|
|
16
|
+
unaffected — it is a keyed `Select: COUNT` query, not a scan.
|
|
17
|
+
- **`create` no longer overwrites.** `put` is conditional on the row not
|
|
18
|
+
already existing, so a colliding id fails loudly rather than replacing data.
|
|
19
|
+
- **Peer range raised to `better-auth >= 1.7.0`**, which is what the adapter is
|
|
20
|
+
now built and conformance-tested against.
|
|
21
|
+
- Draining is capped at `maxPages` (default 25). A query with pages remaining
|
|
22
|
+
at the cap throws rather than returning a partial result — silently dropping
|
|
23
|
+
rows from an auth query is worse than failing.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Atomic uniqueness.** `unique` schema fields (`user.email`, `session.token`,
|
|
28
|
+
`organization.slug`, …) are enforced by DynamoDB itself, via marker rows
|
|
29
|
+
written in the same `TransactWriteItems` as the row they describe. Better
|
|
30
|
+
Auth's own enforcement is a check-then-insert, which two concurrent sign-ups
|
|
31
|
+
can both pass. Opt out with `atomicUniqueness: false`.
|
|
32
|
+
- Note for existing tables: markers are only created by writes made on
|
|
33
|
+
0.2.0+. Uniqueness is enforced going forward; pre-existing duplicates are
|
|
34
|
+
not detected retroactively.
|
|
35
|
+
- **Optimistic concurrency.** Every row carries a hidden revision that guards
|
|
36
|
+
`update`, `delete`, `consumeOne`, and `incrementOne`. A write that lost a
|
|
37
|
+
race retries against the fresh row, then fails with `OptimisticLockError`
|
|
38
|
+
rather than silently clobbering a concurrent change. Rows written before
|
|
39
|
+
0.2.0 are matched on the revision being absent, so they migrate in place on
|
|
40
|
+
first write.
|
|
41
|
+
- **Adapter-managed TTL** (`ttl: { defaultField: "expiresAt" }`). The
|
|
42
|
+
configured date field is projected into a DynamoDB TTL attribute so expired
|
|
43
|
+
sessions and verifications are reaped for free, and the same attribute is
|
|
44
|
+
treated as _logical_ expiry on read — DynamoDB reaps lazily, so without that
|
|
45
|
+
an expired session would keep working until AWS got round to it.
|
|
46
|
+
`ensureSchema`/`generateSchemaFile` provision the TTL setting.
|
|
47
|
+
- **Client injection.** `documentClient` on the adapter config, so your
|
|
48
|
+
application owns credentials, region, middleware, tracing, and marshalling.
|
|
49
|
+
- `pageSize` for per-request `Limit` tuning.
|
|
50
|
+
- Typed errors, all exported and all extending `DynamoDBAdapterError`:
|
|
51
|
+
`UniqueConstraintError`, `OptimisticLockError`, `UnsupportedQueryError`.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **Delimiter safety.** Keys were joined with a bare `#`, so a value containing
|
|
56
|
+
the delimiter could forge another key: with a composite partition key,
|
|
57
|
+
`("a#b", "c")` and `("a", "b#c")` encoded identically, and a sort-key
|
|
58
|
+
`begins_with("cred#")` probe matched a row whose value was literally
|
|
59
|
+
`cred#ential`. Auth tables hold plenty of values that arrive from outside —
|
|
60
|
+
OAuth `accountId`s, organisation slugs, verification identifiers — so this is
|
|
61
|
+
now structurally impossible rather than merely unlikely.
|
|
62
|
+
- **Oversized key values** produced an opaque DynamoDB `ValidationException`.
|
|
63
|
+
Long values are hashed; an overflow can now only come from an absurd model,
|
|
64
|
+
index, or id name, and says so.
|
|
65
|
+
- **Cancelled transactions were all read as uniqueness violations.** DynamoDB
|
|
66
|
+
cancels for throttling, item contention, and validation failures too, so a
|
|
67
|
+
throttled write could be reported to a user as "that email is taken".
|
|
68
|
+
Cancellations are now classified by their per-action code; contention is
|
|
69
|
+
retried, and only a genuine `ConditionalCheckFailed` becomes
|
|
70
|
+
`UniqueConstraintError`.
|
|
71
|
+
- **`incrementOne` could create the row it was told to increment.** DynamoDB's
|
|
72
|
+
`ADD` is an upsert; the write is now conditional on the row existing and
|
|
73
|
+
returns `null` when it does not.
|
|
74
|
+
- **`incrementOne` left stale index rows** when its `set` moved an indexed or
|
|
75
|
+
TTL field, because a native `ADD` cannot re-encode GSI keys. Those cases now
|
|
76
|
+
take the read-modify-write path.
|
|
77
|
+
- **A `where` naming `id` alongside other predicates ignored the others**, so a
|
|
78
|
+
guarded `update`/`delete` could fire against a row that did not qualify.
|
|
79
|
+
- **`select` was ignored** by `findOne`/`findMany`, and needed mapping through
|
|
80
|
+
the schema's `fieldName` overrides.
|
|
81
|
+
- **`id in [...]` fell through to a model scan.** Better Auth batch-loads rows
|
|
82
|
+
it already has ids for (an organisation's members, for one), which DynamoDB
|
|
83
|
+
serves as a bounded multi-get. It is now planned as one.
|
|
84
|
+
- **`count` used the native fast path for id lookups**, which counts a whole
|
|
85
|
+
model or index — answering "how many of these three ids exist" with the size
|
|
86
|
+
of the table.
|
|
87
|
+
- **Case-insensitive equality was served from an index**, which byte-compares
|
|
88
|
+
keys and so missed every row whose casing differed. Those clauses are now
|
|
89
|
+
matched in memory.
|
|
90
|
+
|
|
91
|
+
### Testing
|
|
92
|
+
|
|
93
|
+
- Better Auth's **official adapter conformance suites** (`normal`, `uuid`,
|
|
94
|
+
`caseInsensitive`, `authFlow`) now run against real DynamoDB. They found four
|
|
95
|
+
of the bugs listed above.
|
|
96
|
+
- The e2e suite moved from LocalStack + docker-compose to AWS's own DynamoDB
|
|
97
|
+
Local, started per-run by Testcontainers — nothing to start by hand, no port
|
|
98
|
+
collisions, no container surviving a crashed run.
|
|
99
|
+
- New unit suites cover key-collision resistance, transaction-cancellation
|
|
100
|
+
classification, the page cap, the scan guard, and the store's write paths
|
|
101
|
+
against an in-memory DynamoDB double.
|
|
102
|
+
|
|
3
103
|
## 0.1.1
|
|
4
104
|
|
|
5
105
|
### Patch Changes
|
|
@@ -10,7 +110,7 @@
|
|
|
10
110
|
used by the email-OTP verify flow — and `incrementOne` for atomic guarded
|
|
11
111
|
counter updates. Without them, any consumer on better-auth >=1.7 hit
|
|
12
112
|
`BetterAuthError: Adapter "dynamodb" must implement consumeOne for atomic
|
|
13
|
-
|
|
113
|
+
single-use credential consumption` the first time a plugin exercised that
|
|
14
114
|
path (e.g. `emailOTP().signIn`).
|
|
15
115
|
- `consumeOne` is implemented on the built-in store as a native
|
|
16
116
|
`DeleteCommand` with `ReturnValues: "ALL_OLD"` (atomic delete-and-return);
|
package/README.md
CHANGED
|
@@ -8,7 +8,16 @@ A generic [DynamoDB](https://aws.amazon.com/dynamodb/) adapter for [Better Auth]
|
|
|
8
8
|
- **Bring your own store.** The adapter talks to a small `DynamoStore` seam, so
|
|
9
9
|
you can back it with an existing single-table design (ElectroDB, custom key
|
|
10
10
|
encoding, a shared table) without changing the adapter.
|
|
11
|
-
- **
|
|
11
|
+
- **Atomic uniqueness.** `unique` fields are enforced by DynamoDB itself, in the
|
|
12
|
+
same transaction as the row — not by a check-then-insert two concurrent
|
|
13
|
+
sign-ups can both pass.
|
|
14
|
+
- **Safe under concurrency.** Every row carries a revision that guards updates,
|
|
15
|
+
deletes, and single-use consumes against lost writes.
|
|
16
|
+
- **No hidden scans, no silent truncation.** A query no index can serve fails
|
|
17
|
+
loudly instead of quietly reading the whole model, and pagination that hits
|
|
18
|
+
its cap throws rather than returning part of an answer.
|
|
19
|
+
- **Passes Better Auth's official adapter conformance suites** against real
|
|
20
|
+
DynamoDB.
|
|
12
21
|
- Zero dependencies beyond the AWS SDK.
|
|
13
22
|
|
|
14
23
|
## Install
|
|
@@ -58,6 +67,43 @@ You can also generate a CloudFormation template via the Better Auth CLI
|
|
|
58
67
|
(`npx @better-auth/cli generate`) — the adapter's `createSchema` emits one sized
|
|
59
68
|
to your access patterns.
|
|
60
69
|
|
|
70
|
+
### Expiring sessions and verifications
|
|
71
|
+
|
|
72
|
+
Nothing expires on its own. Point the adapter at your expiry field and DynamoDB
|
|
73
|
+
reaps expired rows for free:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
dynamoAdapter({
|
|
77
|
+
tableName: "better-auth",
|
|
78
|
+
ttl: { defaultField: "expiresAt" },
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pass the same attribute to `ensureSchema({ ..., ttlAttribute: "__ba_ttl" })` —
|
|
83
|
+
the adapter writes the attribute, but only the table setting makes AWS act on
|
|
84
|
+
it. Reads treat it as _logical_ expiry too: DynamoDB reaps lazily, often a day
|
|
85
|
+
or two late, so without that an expired session would keep working until AWS got
|
|
86
|
+
round to deleting it.
|
|
87
|
+
|
|
88
|
+
### Uniqueness
|
|
89
|
+
|
|
90
|
+
Fields the Better Auth schema marks `unique` — `user.email`, `session.token`,
|
|
91
|
+
`organization.slug` — get a marker row written in the same
|
|
92
|
+
`TransactWriteItems` as the row itself, conditional on the marker not already
|
|
93
|
+
existing. That makes uniqueness a property of the database: of two concurrent
|
|
94
|
+
sign-ups for the same email, exactly one commits.
|
|
95
|
+
|
|
96
|
+
Better Auth's own enforcement is a check-then-insert, which both racers can
|
|
97
|
+
pass. Set `atomicUniqueness: false` to fall back to it and halve the write cost
|
|
98
|
+
of creates.
|
|
99
|
+
|
|
100
|
+
Two caveats worth knowing:
|
|
101
|
+
|
|
102
|
+
- Markers are created by writes made on 0.2.0+. Upgrading an existing table
|
|
103
|
+
enforces uniqueness going forward; it does not find duplicates already there.
|
|
104
|
+
- Do not write Better Auth rows into the table with a raw `PutItem`. Entity
|
|
105
|
+
rows, index keys, markers, and TTL attributes have to move together.
|
|
106
|
+
|
|
61
107
|
## Bring your own store
|
|
62
108
|
|
|
63
109
|
Implement `DynamoStore` to run the same adapter against your own table layout.
|
|
@@ -114,23 +160,42 @@ omit it to auto-derive from the schema.
|
|
|
114
160
|
|
|
115
161
|
## Configuration
|
|
116
162
|
|
|
117
|
-
| Option
|
|
118
|
-
|
|
|
119
|
-
| `store`
|
|
120
|
-
| `indexMap`
|
|
121
|
-
| `tableName`
|
|
122
|
-
| `region`
|
|
123
|
-
| `endpoint`
|
|
124
|
-
| `
|
|
163
|
+
| Option | Default | Description |
|
|
164
|
+
| ------------------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `store` | built-in single-table store | Storage backend (`DynamoStore`). |
|
|
166
|
+
| `indexMap` | derived from schema | Access-pattern map. |
|
|
167
|
+
| `tableName` | `DYNAMODB_TABLE_NAME` → `better-auth` | Table name (built-in store). |
|
|
168
|
+
| `region` | SDK default | AWS region (built-in store). |
|
|
169
|
+
| `endpoint` | SDK default | Endpoint override for DynamoDB Local / LocalStack, e.g. `http://localhost:8000` (built-in store). |
|
|
170
|
+
| `documentClient` | created internally | Pre-built `DynamoDBDocumentClient`. Preferred in production, so your app owns credentials, region, middleware, and tracing. |
|
|
171
|
+
| `atomicUniqueness` | `true` | Enforce `unique` fields with transactional marker rows. |
|
|
172
|
+
| `ttl` | disabled | Adapter-managed DynamoDB TTL, e.g. `{ defaultField: "expiresAt" }`. |
|
|
173
|
+
| `maxPages` | `25` | Cap on pages drained per query. Throws at the cap rather than returning a partial result. |
|
|
174
|
+
| `pageSize` | SDK default | Per-request DynamoDB `Limit`. Changes request sizing only, not logical results. |
|
|
175
|
+
| `unsafeAllowScan` | `false` | Allow queries no index can serve, which read every row of the model and filter in memory. |
|
|
176
|
+
| `debugLogs` | `false` | Better Auth debug logging. |
|
|
125
177
|
|
|
126
178
|
## Notes & limitations
|
|
127
179
|
|
|
128
180
|
- IDs are strings (Better Auth generates them); `supportsNumericIds` is `false`.
|
|
129
181
|
- Dates are stored as ISO strings and re-hydrated on read (`supportsDates: false`).
|
|
130
|
-
- No
|
|
182
|
+
- No interactive transactions — DynamoDB has no such API, so the adapter
|
|
183
|
+
reports `transaction: false`. `TransactWriteItems` is used internally for
|
|
184
|
+
single-row atomic operations; multi-row ops run sequentially in small batches.
|
|
185
|
+
- Case-insensitive equality (`mode: "insensitive"`) cannot come from an index,
|
|
186
|
+
because DynamoDB compares keys byte-for-byte. Those clauses are matched in
|
|
187
|
+
memory, so they need `unsafeAllowScan: true` unless another clause in the same
|
|
188
|
+
`where` can be served by an index.
|
|
189
|
+
- Better Auth's verification cleanup issues a range-only
|
|
190
|
+
`deleteMany(expiresAt < now)`, which has no key to work from. Configure `ttl`
|
|
191
|
+
and set `verification: { disableCleanup: true }` — that is the
|
|
192
|
+
DynamoDB-shaped answer to the same problem.
|
|
131
193
|
- The built-in store's derived index map keys on schema field names; custom
|
|
132
194
|
`fieldName` mappings are not yet resolved in derivation (pass an explicit
|
|
133
195
|
`indexMap` if you rename fields).
|
|
196
|
+
- Key values longer than 256 bytes are SHA-256 hashed into the key. Lookups are
|
|
197
|
+
unaffected — both sides hash identically — but a key is no longer always
|
|
198
|
+
readable as plain text when debugging.
|
|
134
199
|
|
|
135
200
|
## License
|
|
136
201
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BetterAuthOptions } from 'better-auth';
|
|
2
2
|
import { AdapterFactory } from 'better-auth/adapters';
|
|
3
|
-
import { DBAdapterDebugLogOption, CleanedWhere } from '@better-auth/core/db/adapter';
|
|
4
3
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
4
|
+
import { DBAdapterDebugLogOption, CleanedWhere } from '@better-auth/core/db/adapter';
|
|
5
5
|
import { BetterAuthDBSchema } from '@better-auth/core/db';
|
|
6
6
|
import { CreateTableCommandInput, DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
7
7
|
|
|
@@ -29,6 +29,14 @@ interface AccessPattern {
|
|
|
29
29
|
* (as `eq` clauses), and leaves the rest to residual in-memory filtering.
|
|
30
30
|
*/
|
|
31
31
|
sk?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* True when this pattern backs a *uniqueness constraint* (a schema field
|
|
34
|
+
* marked `unique`, e.g. `user.email`) rather than a plain lookup. The
|
|
35
|
+
* built-in store enforces these atomically with marker rows; a custom store
|
|
36
|
+
* may ignore the flag and fall back to Better Auth's application-layer
|
|
37
|
+
* check-then-insert.
|
|
38
|
+
*/
|
|
39
|
+
unique?: boolean;
|
|
32
40
|
}
|
|
33
41
|
/** Ordered list of access patterns for a single model, most specific first. */
|
|
34
42
|
type ModelIndexMap = AccessPattern[];
|
|
@@ -129,6 +137,25 @@ interface DynamoStore {
|
|
|
129
137
|
overwrite?: boolean;
|
|
130
138
|
}>;
|
|
131
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Adapter-managed DynamoDB TTL.
|
|
142
|
+
*
|
|
143
|
+
* A configured date field (e.g. `session.expiresAt`) is projected into a
|
|
144
|
+
* numeric epoch-seconds attribute that DynamoDB's own TTL reaper understands,
|
|
145
|
+
* so expired auth rows are deleted for free instead of accumulating.
|
|
146
|
+
*
|
|
147
|
+
* Reads treat the same attribute as *logical* expiry. DynamoDB reaps lazily —
|
|
148
|
+
* often a day or two late — so without that, an expired session would keep
|
|
149
|
+
* working until AWS got round to deleting it.
|
|
150
|
+
*/
|
|
151
|
+
interface TtlOptions {
|
|
152
|
+
/** DynamoDB TTL attribute to write. Defaults to `__ba_ttl`. */
|
|
153
|
+
attributeName?: string;
|
|
154
|
+
/** Per-model date field, e.g. `{ session: "expiresAt" }`. */
|
|
155
|
+
fields?: Record<string, string>;
|
|
156
|
+
/** Date field used for any model not named in `fields`, e.g. `"expiresAt"`. */
|
|
157
|
+
defaultField?: string;
|
|
158
|
+
}
|
|
132
159
|
/**
|
|
133
160
|
* Public configuration for {@link dynamoAdapter}.
|
|
134
161
|
*
|
|
@@ -150,6 +177,41 @@ interface DynamoAdapterConfig {
|
|
|
150
177
|
* at DynamoDB Local or LocalStack, e.g. `http://localhost:4566`.
|
|
151
178
|
*/
|
|
152
179
|
endpoint?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Pre-built DynamoDB document client (used only by the built-in store).
|
|
182
|
+
* Preferred in production: your application keeps ownership of credentials,
|
|
183
|
+
* region, middleware, tracing, and marshalling behaviour.
|
|
184
|
+
*/
|
|
185
|
+
documentClient?: DynamoDBDocumentClient;
|
|
186
|
+
/**
|
|
187
|
+
* Enforce `unique` schema fields with transactional marker rows (used only
|
|
188
|
+
* by the built-in store). Default `true`.
|
|
189
|
+
*/
|
|
190
|
+
atomicUniqueness?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* Maximum DynamoDB pages drained for one logical query. Default 25. When a
|
|
193
|
+
* query still has pages left at the cap, the adapter throws rather than
|
|
194
|
+
* returning a silently truncated result — a partial answer to an auth query
|
|
195
|
+
* is worse than a loud failure.
|
|
196
|
+
*/
|
|
197
|
+
maxPages?: number;
|
|
198
|
+
/**
|
|
199
|
+
* Per-request DynamoDB `Limit` (used only by the built-in store). Pagination
|
|
200
|
+
* is still drained up to `maxPages`, so this changes request sizing only.
|
|
201
|
+
*/
|
|
202
|
+
pageSize?: number;
|
|
203
|
+
/** Adapter-managed DynamoDB TTL. Omit (or `false`) to disable entirely. */
|
|
204
|
+
ttl?: TtlOptions | false;
|
|
205
|
+
/**
|
|
206
|
+
* Allow queries that no index can serve, which fall back to draining every
|
|
207
|
+
* row of the model and filtering in memory. Default `false`, so an access
|
|
208
|
+
* pattern nobody designed for fails at development time instead of quietly
|
|
209
|
+
* costing a full model read on every call.
|
|
210
|
+
*
|
|
211
|
+
* Native `count` is unaffected: it is a keyed `Select: COUNT` query, bounded
|
|
212
|
+
* by `maxPages`, and returns a number rather than every row.
|
|
213
|
+
*/
|
|
214
|
+
unsafeAllowScan?: boolean;
|
|
153
215
|
/** Better Auth debug logging, forwarded to the adapter factory. */
|
|
154
216
|
debugLogs?: DBAdapterDebugLogOption;
|
|
155
217
|
}
|
|
@@ -170,11 +232,67 @@ interface DynamoAdapterConfig {
|
|
|
170
232
|
*/
|
|
171
233
|
declare const dynamoAdapter: (config?: DynamoAdapterConfig) => AdapterFactory<BetterAuthOptions>;
|
|
172
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Error types raised by this adapter.
|
|
237
|
+
*
|
|
238
|
+
* Everything thrown from the adapter core or the built-in store derives from
|
|
239
|
+
* {@link DynamoDBAdapterError}, so a consumer can distinguish "the adapter
|
|
240
|
+
* refused/failed" from a raw AWS SDK error escaping the seam.
|
|
241
|
+
*/
|
|
242
|
+
declare class DynamoDBAdapterError extends Error {
|
|
243
|
+
constructor(message: string, options?: ErrorOptions);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A write lost a race against a uniqueness constraint — another row already
|
|
247
|
+
* holds the value for a `unique` field (e.g. `user.email`, `organization.slug`).
|
|
248
|
+
*/
|
|
249
|
+
declare class UniqueConstraintError extends DynamoDBAdapterError {
|
|
250
|
+
readonly model: string;
|
|
251
|
+
readonly fields: string[];
|
|
252
|
+
constructor(model: string, fields: string[], options?: ErrorOptions);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The requested access pattern cannot be served without a table/model scan, and
|
|
256
|
+
* scans have not been explicitly enabled.
|
|
257
|
+
*/
|
|
258
|
+
declare class UnsupportedQueryError extends DynamoDBAdapterError {
|
|
259
|
+
constructor(message: string);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A guarded write was rejected because the row changed between the adapter's
|
|
263
|
+
* read and its write. Better Auth's own retry/null semantics decide what
|
|
264
|
+
* happens next; this exists so the cause is legible rather than an opaque
|
|
265
|
+
* `TransactionCanceledException`.
|
|
266
|
+
*/
|
|
267
|
+
declare class OptimisticLockError extends DynamoDBAdapterError {
|
|
268
|
+
constructor(model: string, id: string, options?: ErrorOptions);
|
|
269
|
+
}
|
|
270
|
+
/** True for a bare `ConditionalCheckFailedException` from a non-transactional write. */
|
|
271
|
+
declare const isConditionalCheckFailed: (error: unknown) => boolean;
|
|
272
|
+
/** True for any cancelled `TransactWriteItems`, whatever the reason. */
|
|
273
|
+
declare const isTransactionCanceled: (error: unknown) => boolean;
|
|
274
|
+
/**
|
|
275
|
+
* The per-action cancellation codes AWS attaches to a cancelled transaction
|
|
276
|
+
* (e.g. `ConditionalCheckFailed`, `TransactionConflict`, `ThrottlingError`,
|
|
277
|
+
* `ValidationError`, or `None` for actions that were fine).
|
|
278
|
+
*/
|
|
279
|
+
declare function transactionCancellationCodes(error: unknown): string[];
|
|
280
|
+
/**
|
|
281
|
+
* True only when a transaction was cancelled *because a condition failed* —
|
|
282
|
+
* not because of throttling, a transaction conflict, or a validation error.
|
|
283
|
+
*
|
|
284
|
+
* This distinction matters: reporting a throttled write as "email already
|
|
285
|
+
* taken" is a user-visible lie, and it is the failure mode of any code that
|
|
286
|
+
* treats `TransactionCanceledException` as a uniqueness violation wholesale.
|
|
287
|
+
*/
|
|
288
|
+
declare const isConditionalTransactionCanceled: (error: unknown) => boolean;
|
|
289
|
+
|
|
173
290
|
/**
|
|
174
291
|
* A resolved query plan: how the adapter will fetch candidate rows for a
|
|
175
292
|
* `where` clause before applying any residual (in-memory) filtering.
|
|
176
293
|
*
|
|
177
294
|
* - `byId` — a direct primary-key get (the cheapest path).
|
|
295
|
+
* - `byIds` — a bounded set of primary-key gets (`id in [...]`).
|
|
178
296
|
* - `index` — a logical index query with a resolved partition/sort key.
|
|
179
297
|
* - `listByType`— no index matched; list all rows of the model and filter.
|
|
180
298
|
*
|
|
@@ -185,6 +303,10 @@ type QueryPlan = {
|
|
|
185
303
|
kind: "byId";
|
|
186
304
|
id: string;
|
|
187
305
|
residual: CleanedWhere[];
|
|
306
|
+
} | {
|
|
307
|
+
kind: "byIds";
|
|
308
|
+
ids: string[];
|
|
309
|
+
residual: CleanedWhere[];
|
|
188
310
|
} | {
|
|
189
311
|
kind: "index";
|
|
190
312
|
index: string;
|
|
@@ -219,8 +341,24 @@ interface SingleTableStoreOptions {
|
|
|
219
341
|
endpoint?: string;
|
|
220
342
|
/** Resolved logical access-pattern map (derived or user-supplied). */
|
|
221
343
|
indexMap: IndexMap;
|
|
222
|
-
/**
|
|
344
|
+
/**
|
|
345
|
+
* Pre-built document client. Preferred in production, so your application
|
|
346
|
+
* owns credentials, region, middleware, tracing, and marshalling.
|
|
347
|
+
*/
|
|
223
348
|
documentClient?: DynamoDBDocumentClient;
|
|
349
|
+
/**
|
|
350
|
+
* Enforce `unique` schema fields with transactional marker rows. Default
|
|
351
|
+
* `true`. Turning it off halves the write cost of creates and restores
|
|
352
|
+
* Better Auth's application-layer check-then-insert, which can admit
|
|
353
|
+
* duplicates under concurrency.
|
|
354
|
+
*/
|
|
355
|
+
atomicUniqueness?: boolean;
|
|
356
|
+
/** Max DynamoDB pages drained for one internal count. Default 25. */
|
|
357
|
+
maxPages?: number;
|
|
358
|
+
/** Optional per-request DynamoDB `Limit`. Does not change logical results. */
|
|
359
|
+
pageSize?: number;
|
|
360
|
+
/** Adapter-managed DynamoDB TTL. Omit (or `false`) to disable entirely. */
|
|
361
|
+
ttl?: TtlOptions | false;
|
|
224
362
|
}
|
|
225
363
|
/**
|
|
226
364
|
* A zero-dependency (beyond the AWS SDK) DynamoDB store for Better Auth.
|
|
@@ -230,6 +368,10 @@ interface SingleTableStoreOptions {
|
|
|
230
368
|
* through logical index names only. Works with any Better Auth model or plugin
|
|
231
369
|
* whose looked-up fields are described by the (typically schema-derived) index
|
|
232
370
|
* map.
|
|
371
|
+
*
|
|
372
|
+
* Every mutation is guarded: creates are conditional on the row not already
|
|
373
|
+
* existing, updates and deletes carry an optimistic revision check, and
|
|
374
|
+
* uniqueness markers move in the same transaction as the row they describe.
|
|
233
375
|
*/
|
|
234
376
|
declare function createSingleTableStore(opts: SingleTableStoreOptions): DynamoStore;
|
|
235
377
|
|
|
@@ -257,6 +399,12 @@ declare function ensureSchema(opts: {
|
|
|
257
399
|
client: DynamoDBClient;
|
|
258
400
|
tableName: string;
|
|
259
401
|
lookupSlots: number;
|
|
402
|
+
/**
|
|
403
|
+
* Enable DynamoDB TTL on this attribute. Pass the same value the adapter is
|
|
404
|
+
* configured with (`ttl.attributeName`, default `__ba_ttl`) — the adapter
|
|
405
|
+
* writes the attribute, but only the table setting makes AWS act on it.
|
|
406
|
+
*/
|
|
407
|
+
ttlAttribute?: string;
|
|
260
408
|
}): Promise<void>;
|
|
261
409
|
/**
|
|
262
410
|
* Better Auth CLI `generate` hook: emit a portable CloudFormation template for
|
|
@@ -268,12 +416,16 @@ declare function generateSchemaFile(opts: {
|
|
|
268
416
|
tableName: string;
|
|
269
417
|
lookupSlots: number;
|
|
270
418
|
file?: string;
|
|
419
|
+
/** When set, the template enables DynamoDB TTL on this attribute. */
|
|
420
|
+
ttlAttribute?: string;
|
|
271
421
|
}): {
|
|
272
422
|
code: string;
|
|
273
423
|
path: string;
|
|
274
424
|
overwrite: boolean;
|
|
275
425
|
};
|
|
276
426
|
|
|
427
|
+
/** Default DynamoDB TTL attribute (epoch seconds). Configurable per store. */
|
|
428
|
+
declare const DEFAULT_TTL_ATTRIBUTE = "__ba_ttl";
|
|
277
429
|
/** Physical slot assignment: model -> (logical index name -> GSI slot number). */
|
|
278
430
|
interface SlotAssignment {
|
|
279
431
|
slots: Record<string, Record<string, number>>;
|
|
@@ -286,4 +438,4 @@ interface SlotAssignment {
|
|
|
286
438
|
*/
|
|
287
439
|
declare function assignSlots(indexMap: IndexMap): SlotAssignment;
|
|
288
440
|
|
|
289
|
-
export { type AccessPattern, type DynamoAdapterConfig, type DynamoStore, type IndexMap, type ModelIndexMap, type QueryPage, type QueryPlan, type SingleTableStoreOptions, type StoreItem, assignSlots, buildTableDefinition, createSingleTableStore, deriveIndexMap, dynamoAdapter, ensureSchema, generateSchemaFile, matchesResidual, planQuery };
|
|
441
|
+
export { type AccessPattern, DEFAULT_TTL_ATTRIBUTE, type DynamoAdapterConfig, DynamoDBAdapterError, type DynamoStore, type IndexMap, type ModelIndexMap, OptimisticLockError, type QueryPage, type QueryPlan, type SingleTableStoreOptions, type StoreItem, type TtlOptions, UniqueConstraintError, UnsupportedQueryError, assignSlots, buildTableDefinition, createSingleTableStore, deriveIndexMap, dynamoAdapter, ensureSchema, generateSchemaFile, isConditionalCheckFailed, isConditionalTransactionCanceled, isTransactionCanceled, matchesResidual, planQuery, transactionCancellationCodes };
|