@open-aih/payload-ddb 3.1.0 → 4.2.0-beta.20261113193102
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/README.md +256 -26
- package/dist/count.d.ts.map +1 -1
- package/dist/count.js +4 -2
- package/dist/count.js.map +1 -1
- package/dist/countGlobalVersions.d.ts.map +1 -1
- package/dist/countGlobalVersions.js +6 -1
- package/dist/countGlobalVersions.js.map +1 -1
- package/dist/countVersions.d.ts.map +1 -1
- package/dist/countVersions.js +6 -1
- package/dist/countVersions.js.map +1 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +29 -15
- package/dist/create.js.map +1 -1
- package/dist/createGlobal.d.ts.map +1 -1
- package/dist/createGlobal.js +2 -14
- package/dist/createGlobal.js.map +1 -1
- package/dist/createGlobalVersion.d.ts +1 -1
- package/dist/createGlobalVersion.d.ts.map +1 -1
- package/dist/createGlobalVersion.js +15 -49
- package/dist/createGlobalVersion.js.map +1 -1
- package/dist/createVersion.d.ts +11 -7
- package/dist/createVersion.d.ts.map +1 -1
- package/dist/createVersion.js +24 -67
- package/dist/createVersion.js.map +1 -1
- package/dist/deleteMany.d.ts +1 -6
- package/dist/deleteMany.d.ts.map +1 -1
- package/dist/deleteMany.js +11 -19
- package/dist/deleteMany.js.map +1 -1
- package/dist/deleteOne.d.ts.map +1 -1
- package/dist/deleteOne.js +20 -24
- package/dist/deleteOne.js.map +1 -1
- package/dist/deleteVersions.d.ts.map +1 -1
- package/dist/deleteVersions.js +12 -14
- package/dist/deleteVersions.js.map +1 -1
- package/dist/find.d.ts +16 -13
- package/dist/find.d.ts.map +1 -1
- package/dist/find.js +97 -23
- package/dist/find.js.map +1 -1
- package/dist/findDistinct.d.ts.map +1 -1
- package/dist/findDistinct.js +3 -1
- package/dist/findDistinct.js.map +1 -1
- package/dist/findGlobal.d.ts.map +1 -1
- package/dist/findGlobal.js +3 -2
- package/dist/findGlobal.js.map +1 -1
- package/dist/findGlobalVersions.d.ts.map +1 -1
- package/dist/findGlobalVersions.js +10 -2
- package/dist/findGlobalVersions.js.map +1 -1
- package/dist/findOne.d.ts.map +1 -1
- package/dist/findOne.js +17 -6
- package/dist/findOne.js.map +1 -1
- package/dist/findVersions.d.ts.map +1 -1
- package/dist/findVersions.js +10 -2
- package/dist/findVersions.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +5 -0
- package/dist/init.js.map +1 -1
- package/dist/queryDrafts.d.ts.map +1 -1
- package/dist/queryDrafts.js +29 -10
- package/dist/queryDrafts.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/updateGlobal.d.ts +5 -4
- package/dist/updateGlobal.d.ts.map +1 -1
- package/dist/updateGlobal.js +22 -36
- package/dist/updateGlobal.js.map +1 -1
- package/dist/updateGlobalVersion.d.ts.map +1 -1
- package/dist/updateGlobalVersion.js +3 -49
- package/dist/updateGlobalVersion.js.map +1 -1
- package/dist/updateJobs.d.ts +21 -0
- package/dist/updateJobs.d.ts.map +1 -0
- package/dist/updateJobs.js +83 -0
- package/dist/updateJobs.js.map +1 -0
- package/dist/updateMany.d.ts +9 -3
- package/dist/updateMany.d.ts.map +1 -1
- package/dist/updateMany.js +45 -30
- package/dist/updateMany.js.map +1 -1
- package/dist/updateOne.d.ts +6 -1
- package/dist/updateOne.d.ts.map +1 -1
- package/dist/updateOne.js +47 -49
- package/dist/updateOne.js.map +1 -1
- package/dist/updateVersion.d.ts.map +1 -1
- package/dist/updateVersion.js +3 -52
- package/dist/updateVersion.js.map +1 -1
- package/dist/upsert.d.ts +6 -0
- package/dist/upsert.d.ts.map +1 -1
- package/dist/upsert.js +55 -41
- package/dist/upsert.js.map +1 -1
- package/dist/utilities/applySorts.d.ts +5 -2
- package/dist/utilities/applySorts.d.ts.map +1 -1
- package/dist/utilities/applySorts.js +11 -6
- package/dist/utilities/applySorts.js.map +1 -1
- package/dist/utilities/backfillIndexes.d.ts +71 -0
- package/dist/utilities/backfillIndexes.d.ts.map +1 -0
- package/dist/utilities/backfillIndexes.js +149 -0
- package/dist/utilities/backfillIndexes.js.map +1 -0
- package/dist/utilities/batchGet.d.ts +15 -0
- package/dist/utilities/batchGet.d.ts.map +1 -0
- package/dist/utilities/batchGet.js +56 -0
- package/dist/utilities/batchGet.js.map +1 -0
- package/dist/utilities/buildFilterExpression.d.ts +29 -14
- package/dist/utilities/buildFilterExpression.d.ts.map +1 -1
- package/dist/utilities/buildFilterExpression.js +55 -14
- package/dist/utilities/buildFilterExpression.js.map +1 -1
- package/dist/utilities/castWhere.d.ts +84 -0
- package/dist/utilities/castWhere.d.ts.map +1 -0
- package/dist/utilities/castWhere.js +254 -0
- package/dist/utilities/castWhere.js.map +1 -0
- package/dist/utilities/draftsFallback.d.ts.map +1 -1
- package/dist/utilities/draftsFallback.js +3 -6
- package/dist/utilities/draftsFallback.js.map +1 -1
- package/dist/utilities/fieldIndexes.d.ts +130 -0
- package/dist/utilities/fieldIndexes.d.ts.map +1 -0
- package/dist/utilities/fieldIndexes.js +286 -0
- package/dist/utilities/fieldIndexes.js.map +1 -0
- package/dist/utilities/findFirst.d.ts +5 -2
- package/dist/utilities/findFirst.d.ts.map +1 -1
- package/dist/utilities/findFirst.js +19 -46
- package/dist/utilities/findFirst.js.map +1 -1
- package/dist/utilities/geo.d.ts +36 -0
- package/dist/utilities/geo.d.ts.map +1 -0
- package/dist/utilities/geo.js +108 -0
- package/dist/utilities/geo.js.map +1 -0
- package/dist/utilities/indexLookup.d.ts +73 -0
- package/dist/utilities/indexLookup.d.ts.map +1 -0
- package/dist/utilities/indexLookup.js +361 -0
- package/dist/utilities/indexLookup.js.map +1 -0
- package/dist/utilities/indexState.d.ts +13 -0
- package/dist/utilities/indexState.d.ts.map +1 -0
- package/dist/utilities/indexState.js +199 -0
- package/dist/utilities/indexState.js.map +1 -0
- package/dist/utilities/mapWithConcurrency.d.ts +12 -0
- package/dist/utilities/mapWithConcurrency.d.ts.map +1 -0
- package/dist/utilities/mapWithConcurrency.js +32 -0
- package/dist/utilities/mapWithConcurrency.js.map +1 -0
- package/dist/utilities/matchesWhere.d.ts +25 -5
- package/dist/utilities/matchesWhere.d.ts.map +1 -1
- package/dist/utilities/matchesWhere.js +150 -38
- package/dist/utilities/matchesWhere.js.map +1 -1
- package/dist/utilities/operators.d.ts +22 -12
- package/dist/utilities/operators.d.ts.map +1 -1
- package/dist/utilities/operators.js +36 -31
- package/dist/utilities/operators.js.map +1 -1
- package/dist/utilities/optimistic.d.ts +29 -0
- package/dist/utilities/optimistic.d.ts.map +1 -0
- package/dist/utilities/optimistic.js +58 -0
- package/dist/utilities/optimistic.js.map +1 -0
- package/dist/utilities/prepareWhere.d.ts +63 -0
- package/dist/utilities/prepareWhere.d.ts.map +1 -0
- package/dist/utilities/prepareWhere.js +198 -0
- package/dist/utilities/prepareWhere.js.map +1 -0
- package/dist/utilities/queryCount.d.ts +14 -4
- package/dist/utilities/queryCount.d.ts.map +1 -1
- package/dist/utilities/queryCount.js +23 -46
- package/dist/utilities/queryCount.js.map +1 -1
- package/dist/utilities/queryMatching.d.ts +18 -10
- package/dist/utilities/queryMatching.d.ts.map +1 -1
- package/dist/utilities/queryMatching.js +26 -57
- package/dist/utilities/queryMatching.js.map +1 -1
- package/dist/utilities/queryPartition.d.ts +26 -0
- package/dist/utilities/queryPartition.d.ts.map +1 -0
- package/dist/utilities/queryPartition.js +55 -0
- package/dist/utilities/queryPartition.js.map +1 -0
- package/dist/utilities/resolveJoins.d.ts +37 -0
- package/dist/utilities/resolveJoins.d.ts.map +1 -0
- package/dist/utilities/resolveJoins.js +195 -0
- package/dist/utilities/resolveJoins.js.map +1 -0
- package/dist/utilities/stripInternalKeys.d.ts +17 -3
- package/dist/utilities/stripInternalKeys.d.ts.map +1 -1
- package/dist/utilities/stripInternalKeys.js +25 -4
- package/dist/utilities/stripInternalKeys.js.map +1 -1
- package/dist/utilities/uniqueGuards.d.ts +73 -0
- package/dist/utilities/uniqueGuards.d.ts.map +1 -0
- package/dist/utilities/uniqueGuards.js +132 -0
- package/dist/utilities/uniqueGuards.js.map +1 -0
- package/dist/utilities/updateVersionRow.d.ts +19 -0
- package/dist/utilities/updateVersionRow.d.ts.map +1 -0
- package/dist/utilities/updateVersionRow.js +41 -0
- package/dist/utilities/updateVersionRow.js.map +1 -0
- package/dist/utilities/versionIndex.d.ts +45 -0
- package/dist/utilities/versionIndex.d.ts.map +1 -0
- package/dist/utilities/versionIndex.js +103 -0
- package/dist/utilities/versionIndex.js.map +1 -0
- package/dist/utilities/writeErrors.d.ts +11 -0
- package/dist/utilities/writeErrors.d.ts.map +1 -0
- package/dist/utilities/writeErrors.js +27 -0
- package/dist/utilities/writeErrors.js.map +1 -0
- package/dist/utilities/writeItems.d.ts +167 -0
- package/dist/utilities/writeItems.d.ts.map +1 -0
- package/dist/utilities/writeItems.js +675 -0
- package/dist/utilities/writeItems.js.map +1 -0
- package/package.json +21 -14
package/README.md
CHANGED
|
@@ -3,22 +3,37 @@
|
|
|
3
3
|
An unofficial database adapter for [PayloadCMS](https://payloadcms.com) that
|
|
4
4
|
stores data in [Amazon DynamoDB](https://aws.amazon.com/dynamodb/).
|
|
5
5
|
|
|
6
|
-
> **Status:** all `BaseDatabaseAdapter` methods are implemented end-to-end
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
> shared table
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
6
|
+
> **Status:** all `BaseDatabaseAdapter` methods are implemented end-to-end:
|
|
7
|
+
> CRUD, drafts, versions, globals, localization, every `where` operator, join
|
|
8
|
+
> fields and unique fields. The current strategy is "correct first, optimize
|
|
9
|
+
> later" — a single shared table where lookups by id, unique field or indexed
|
|
10
|
+
> field go straight to the matching rows, and every other filter reads the
|
|
11
|
+
> collection's partition. Suitable for development and small-to-medium
|
|
12
|
+
> workloads; large collections will want the optimizations listed under
|
|
13
|
+
> *Known limits*.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- **Payload 3.x.** The peer range is `payload >=3.0.0`; the test suite runs
|
|
18
|
+
against 3.93.
|
|
19
|
+
- **Node.js as your Payload version requires.** Payload 3.92 and later need
|
|
20
|
+
Node 22.23.4+ or 24.22.1+. This repo's CI runs on Node 24.
|
|
13
21
|
|
|
14
22
|
## Installation
|
|
15
23
|
|
|
24
|
+
The package is published under two names with the same contents:
|
|
25
|
+
|
|
16
26
|
```bash
|
|
27
|
+
# Public npm registry — minor and major releases
|
|
28
|
+
pnpm add @open-aih/payload-ddb
|
|
29
|
+
|
|
30
|
+
# GitHub Packages — every release, including patches
|
|
17
31
|
pnpm add @aih-software/payload-ddb
|
|
18
|
-
# or
|
|
19
|
-
npm install @aih-software/payload-ddb
|
|
20
32
|
```
|
|
21
33
|
|
|
34
|
+
The examples below import from `@aih-software/payload-ddb`; use
|
|
35
|
+
`@open-aih/payload-ddb` instead if that is the one you installed.
|
|
36
|
+
|
|
22
37
|
## Quick start (DynamoDB Local)
|
|
23
38
|
|
|
24
39
|
```ts
|
|
@@ -75,6 +90,8 @@ value in the same physical table.
|
|
|
75
90
|
| `translateConfig` | `TranslateConfig` | `removeUndefinedValues: true`, `convertClassInstanceToMap: true` | Marshalling options forwarded to `DynamoDBDocumentClient.from`. |
|
|
76
91
|
| `tableName` | `string` | `'payload'` | The single DynamoDB table backing every collection, global, and versions stream. |
|
|
77
92
|
| `ensureTables` | `boolean` | `false` | When true, auto-creates the table at init if it doesn't exist. Dev-loop convenience; turn off in production. |
|
|
93
|
+
| `migrationDir` | `string` | `'migrations'` | Directory Payload reads and writes migration files in. |
|
|
94
|
+
| `indexes` | `boolean` | `true` | Maintain index rows for `index: true` fields and answer lookups by an indexed or unique field from them instead of reading the collection. See *Indexes*. |
|
|
78
95
|
|
|
79
96
|
## Data layout
|
|
80
97
|
|
|
@@ -88,42 +105,232 @@ value in the same physical table.
|
|
|
88
105
|
- **Sort key is always String.** Number-typed collection ids are coerced to
|
|
89
106
|
strings for `sk` while the original numeric `id` attribute is preserved on
|
|
90
107
|
the row.
|
|
91
|
-
- **
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
- **Revision counter.** Every row also carries `_rev`, bumped on each write
|
|
109
|
+
and stripped on read. Updates are conditional on it, so two writers can't
|
|
110
|
+
silently overwrite each other.
|
|
111
|
+
- **Unique guards:** `pk = "<slug>#unique#<field path>"`, `sk = "<value>"`,
|
|
112
|
+
one small row per value of a `unique: true` field. See *Unique fields*.
|
|
113
|
+
- **Index rows:** `pk = "<slug>#index#<field path>"`,
|
|
114
|
+
`sk = "<value key>#<id>"`, one small row per value of an `index: true`
|
|
115
|
+
field. Value keys sort the way the values do. See *Indexes*.
|
|
116
|
+
- **Latest-version pointers:** `pk = "<slug>_versions#latest"`,
|
|
117
|
+
`sk = "<parent key>"`, one row per versioned document naming its latest
|
|
118
|
+
version.
|
|
119
|
+
- **Index markers:** `pk = "payload-ddb#indexes"`, one row per index that is
|
|
120
|
+
known to cover every document.
|
|
121
|
+
- **No GSIs or LSIs.** Indexes are ordinary rows in the same table, which is
|
|
122
|
+
what lets them be read with strong consistency. A filter no index can
|
|
123
|
+
answer is a `Query` scoped by `pk`, with its predicates pushed to
|
|
124
|
+
`FilterExpression` (DynamoDB still reads every row in the partition before
|
|
125
|
+
filtering).
|
|
94
126
|
|
|
95
127
|
## What's implemented
|
|
96
128
|
|
|
97
129
|
All `BaseDatabaseAdapter` methods: `create`, `find`, `findOne`, `findDistinct`,
|
|
98
130
|
`count`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `upsert`,
|
|
99
131
|
`createGlobal`/`findGlobal`/`updateGlobal`, the five collection version methods,
|
|
100
|
-
the four global version methods, and `
|
|
132
|
+
the four global version methods, `queryDrafts`, and `updateJobs`.
|
|
133
|
+
|
|
134
|
+
### Queries
|
|
135
|
+
|
|
136
|
+
Every Payload `where` operator is supported: `equals`, `not_equals`,
|
|
137
|
+
`greater_than`, `greater_than_equal`, `less_than`, `less_than_equal`, `in`,
|
|
138
|
+
`not_in`, `all`, `exists`, `like`, `not_like`, `contains`, `near`, `within`,
|
|
139
|
+
`intersects`, plus `and` / `or` composition.
|
|
140
|
+
|
|
141
|
+
- **Pushdown where possible, JS for the rest.** Clauses DynamoDB can express
|
|
142
|
+
go into a `FilterExpression`. The others — the case-insensitive text
|
|
143
|
+
operators (`like`, `not_like`, `contains`), `all`, the geo operators, and
|
|
144
|
+
any path that runs through a list — are evaluated in JS on the rows that
|
|
145
|
+
come back. A `where` that mixes both kinds still narrows the read
|
|
146
|
+
server-side.
|
|
147
|
+
- **Lists behave as in MongoDB.** A `hasMany` field, an `array` or `blocks`
|
|
148
|
+
field, or a list inside `json` is transparent in a path: `tags equals 'x'`
|
|
149
|
+
matches when any element is `'x'`, `items.label equals 'x'` when any row
|
|
150
|
+
has that label. Negative operators hold only when no element matches.
|
|
151
|
+
- **Relationship paths.** `author.name equals 'Ada'` is resolved by querying
|
|
152
|
+
the related collection and filtering on the ids it returns; multi-hop paths
|
|
153
|
+
work the same way.
|
|
154
|
+
- **Localized fields.** With localization enabled, filters and sorts on a
|
|
155
|
+
localized field address the request's locale (`title` → `title.en`), or the
|
|
156
|
+
locale written into the path.
|
|
157
|
+
- **Operand casting.** Operands are cast to the field's stored type, so
|
|
158
|
+
filters sent as strings (REST query strings, admin list filters) behave:
|
|
159
|
+
numeric strings on `number` fields, `'true'`/`'false'` on `checkbox` fields,
|
|
160
|
+
date strings on `date` fields, ids on relationships to numeric-id
|
|
161
|
+
collections, and comma-delimited lists for `in` / `not_in` / `all`.
|
|
162
|
+
DynamoDB compares strictly by type, so an operand the adapter can't cast
|
|
163
|
+
(an unknown path) is compared as given.
|
|
164
|
+
- **`exists`** treats a field stored as `null` the same as a missing one, and
|
|
165
|
+
for text-like fields an empty string as well.
|
|
166
|
+
- **Geo operators** work on `point` fields. Distances are great-circle
|
|
167
|
+
metres; polygon tests are planar.
|
|
168
|
+
|
|
169
|
+
### Indexes
|
|
170
|
+
|
|
171
|
+
A filter normally reads the collection's whole partition. These clauses avoid
|
|
172
|
+
that and read only the matching documents:
|
|
173
|
+
|
|
174
|
+
| Clause | Answered from |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `id` `equals` / `in` | the documents' own keys |
|
|
177
|
+
| `equals` / `in` on a `unique: true` field | the field's unique guards |
|
|
178
|
+
| `equals` / `in` on an `index: true` field | index rows |
|
|
179
|
+
| `greater_than(_equal)` / `less_than(_equal)` on an `index: true` field | index rows, which are kept in value order |
|
|
180
|
+
|
|
181
|
+
The clause has to hold for every result: at the top level or inside `and`,
|
|
182
|
+
not on one side of an `or`. The rest of the `where` is then evaluated on the
|
|
183
|
+
documents it selects. This covers login by `email`, pages by `slug`,
|
|
184
|
+
relationship population (`id in [...]`), filters on a relationship, join
|
|
185
|
+
fields, and date or number ranges.
|
|
186
|
+
|
|
187
|
+
- **Which fields.** `index: true` on a `text`, `email`, `number`, `date`,
|
|
188
|
+
`relationship` or `upload` field — including `hasMany` fields (one row per
|
|
189
|
+
element), fields in groups and named tabs, and localized fields (per
|
|
190
|
+
locale). Payload sets the flag itself on the field a join points at.
|
|
191
|
+
Not indexed: fields in arrays or blocks, polymorphic relationships, types
|
|
192
|
+
where equality rarely narrows a read (`checkbox`, `select`), and the
|
|
193
|
+
timestamps Payload manages (`createdAt`, `updatedAt`, `deletedAt`), where
|
|
194
|
+
an index would turn every insert or update into a transaction.
|
|
195
|
+
- **Sorted pages.** A `find` sorted by one single-valued indexed field walks
|
|
196
|
+
that field's index in order and stops when the page is full, instead of
|
|
197
|
+
reading and sorting the collection. With `pagination: false` nothing else
|
|
198
|
+
is read. With pagination, the total comes from a `COUNT` pass over the
|
|
199
|
+
partition, which returns no documents but is still billed for the read.
|
|
200
|
+
Not used on drafts-enabled collections, for sorts on several fields, or
|
|
201
|
+
for pages more than 2,000 documents deep.
|
|
202
|
+
- **Consistency.** Index rows are written in the same transaction as their
|
|
203
|
+
document and read with strong consistency, so a lookup sees a document as
|
|
204
|
+
soon as it is written — which a DynamoDB GSI can't promise.
|
|
205
|
+
- **Write cost.** A write that adds or removes index rows is a transaction:
|
|
206
|
+
twice the write units for the document, plus the rows. An update that
|
|
207
|
+
leaves the indexed fields alone costs what it did before. A single-valued
|
|
208
|
+
indexed field has a row for every document, blank ones included, so
|
|
209
|
+
inserts into a collection with one are always transactions.
|
|
210
|
+
- **Large matches.** A lookup that finds more than 256 candidate documents
|
|
211
|
+
falls back to reading the partition, which is the cheaper read by then.
|
|
212
|
+
- **Opting out.** `indexes: false` stops the index rows, the pointers and
|
|
213
|
+
the routed lookups. Lookups by `id` and unique enforcement are unaffected.
|
|
214
|
+
Every instance sharing a table must use the same setting.
|
|
215
|
+
|
|
216
|
+
**Versions.** Version rows are indexed by their `parent`, so listing or
|
|
217
|
+
pruning one document's versions reads only those. Each versioned document
|
|
218
|
+
(and each versioned global) also has a pointer to its latest version: saving
|
|
219
|
+
finds the version to demote in two key reads, and listing drafts fetches one
|
|
220
|
+
version per document rather than every version of every document.
|
|
221
|
+
|
|
222
|
+
**Existing data.** An index is only used once it is known to cover every
|
|
223
|
+
row. A partition that is empty when the adapter starts qualifies straight
|
|
224
|
+
away, which covers new tables and new collections. For one that already has
|
|
225
|
+
rows — after upgrading, or after adding `index: true` to a field — run the
|
|
226
|
+
backfill once; until then its lookups keep reading the partition:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { backfillIndexes } from '@aih-software/payload-ddb'
|
|
230
|
+
|
|
231
|
+
const report = await backfillIndexes(payload)
|
|
232
|
+
// report.collections[slug].ready lists the indexes now in use
|
|
233
|
+
// report.versions[slug] covers that collection's or global's versions
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The backfill is safe to run while the application is serving, and safe to
|
|
237
|
+
re-run. It also removes stray index rows, which an interrupted write can
|
|
238
|
+
leave behind (lookups ignore them, so they cost a little space, not
|
|
239
|
+
correctness). When upgrading from a version before 4.2, finish the rollout
|
|
240
|
+
first: older instances write rows without their index rows, so run the
|
|
241
|
+
backfill once after the last one is gone.
|
|
242
|
+
|
|
243
|
+
### Join fields
|
|
244
|
+
|
|
245
|
+
`join` fields are resolved for `find`, `findByID` and draft queries: one
|
|
246
|
+
lookup per target collection for all returned documents, grouped in memory,
|
|
247
|
+
with `limit`, `page`, `sort`, `where` and `count` applied per parent document.
|
|
248
|
+
The lookup uses the index on the field the join points at, so it doesn't read
|
|
249
|
+
the target collection's partition. Polymorphic joins (`collection: [...]`)
|
|
250
|
+
return `{ relationTo, value }` entries. Joined documents are read from the
|
|
251
|
+
target's published rows.
|
|
252
|
+
|
|
253
|
+
### Write guarantees
|
|
254
|
+
|
|
255
|
+
- **`create` is insert-only.** An id that is already taken is rejected with a
|
|
256
|
+
`ValidationError` on `id` rather than replacing the existing document.
|
|
257
|
+
- **No lost updates.** Updates are read-merge-write, conditional on the row's
|
|
258
|
+
revision. If another writer gets in first, the patch is re-applied to the
|
|
259
|
+
fresh row, so concurrent updates to different fields both land. This holds
|
|
260
|
+
per adapter call: Payload's own `update` operation fills the fields missing
|
|
261
|
+
from a patch with the values it read beforehand, so two concurrent
|
|
262
|
+
`payload.update` calls on one document can still overwrite each other.
|
|
263
|
+
- **One latest version.** Creating a version demotes the previous latest and
|
|
264
|
+
moves the document's pointer in one transaction that checks nobody else got
|
|
265
|
+
there first, so saving a document several times at once still leaves
|
|
266
|
+
exactly one version marked latest.
|
|
267
|
+
- **Jobs are claimed once.** `updateJobs` uses the same check, so two workers
|
|
268
|
+
polling the queue can't both take the same job.
|
|
269
|
+
- **Bulk writes are bounded.** `updateMany`, `deleteMany` and `deleteVersions`
|
|
270
|
+
keep at most 25 requests in flight.
|
|
271
|
+
- **Oversized documents fail clearly.** DynamoDB caps an item at 400 KB;
|
|
272
|
+
exceeding it raises a 413 naming the document instead of a bare
|
|
273
|
+
`ValidationException`.
|
|
274
|
+
|
|
275
|
+
### Unique fields
|
|
276
|
+
|
|
277
|
+
`unique: true` is enforced for scalar fields at the top level or inside groups
|
|
278
|
+
and named tabs, including localized fields (per locale) and an auth
|
|
279
|
+
collection's `email` / `username`. Each value is claimed by a guard row
|
|
280
|
+
written in the same transaction as the document, so a duplicate is rejected
|
|
281
|
+
with a `ValidationError` on the field — also under concurrent writes. Blank
|
|
282
|
+
values (`null`, `''`) are not guarded. Unique fields inside arrays or blocks,
|
|
283
|
+
and `hasMany` fields, are not enforced.
|
|
284
|
+
|
|
285
|
+
Writes to a collection with unique fields are transactional and cost twice the
|
|
286
|
+
write units.
|
|
101
287
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
288
|
+
Documents written before this was enforced have no guards until they are next
|
|
289
|
+
saved. `backfillIndexes` (see *Indexes*) covers them in the same pass:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const report = await backfillIndexes(payload)
|
|
293
|
+
// report.collections[slug].conflicts lists pre-existing duplicates
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A unique field with unresolved duplicates stays out of use for lookups until
|
|
297
|
+
they are fixed and the pass is run again. `backfillUniqueGuards` is kept as an
|
|
298
|
+
alias.
|
|
108
299
|
|
|
109
300
|
## Known limits / future work
|
|
110
301
|
|
|
111
|
-
- **
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
302
|
+
- **Unindexed filters and sorts read the partition.** That includes the
|
|
303
|
+
default sort on `createdAt`, sorts on several fields, and any filter with
|
|
304
|
+
no indexed clause. The matches are sorted and paged in memory.
|
|
305
|
+
- **Totals are counted.** A paginated `find` that reads its page through an
|
|
306
|
+
index still counts the matches with a pass over the partition.
|
|
307
|
+
- **Drafts are listed in full.** A drafts listing fetches the latest version
|
|
308
|
+
of every document — one read each, not the whole versions partition — and
|
|
309
|
+
then filters, sorts and pages in memory. `find` on a drafts-enabled
|
|
310
|
+
collection does the same for documents it hasn't already matched.
|
|
311
|
+
- **A document's versions are read together.** Payload looks up a document's
|
|
312
|
+
(or global's) newest version by sort before each save, which reads all of
|
|
313
|
+
that document's versions — bounded by `versions.maxPerDoc`.
|
|
117
314
|
- **Payload-level transactions are no-op.** `beginTransaction` returns `null`;
|
|
118
315
|
commit/rollback do nothing. Per-method atomicity (e.g. `createVersion`'s
|
|
119
316
|
flip+put) uses DynamoDB `TransactWriteItems` already; full Payload-context
|
|
120
317
|
transaction buffering across multiple adapter calls is its own milestone.
|
|
318
|
+
- **Relationship paths read the related collection.** `author.name equals
|
|
319
|
+
'Ada'` first finds the matching authors — a partition read unless `name` is
|
|
320
|
+
indexed — and then looks the documents up through the index on `author`,
|
|
321
|
+
when it has one.
|
|
322
|
+
- **Joins don't see unpublished drafts** of the joined collection.
|
|
323
|
+
- **`locale: 'all'`** in a filter or sort falls back to the default locale.
|
|
324
|
+
- **`like` matches its operand as one substring.** Payload's Mongo adapter
|
|
325
|
+
splits the operand into words and requires each of them to match.
|
|
326
|
+
- **Items are limited to 400 KB**, including a version row's full snapshot.
|
|
121
327
|
|
|
122
328
|
## Development
|
|
123
329
|
|
|
124
330
|
The package ships with a vitest integration test harness that boots Payload
|
|
125
331
|
against DynamoDB Local. Each spec file uses a unique table name so vitest
|
|
126
|
-
workers can run in parallel against a single shared container
|
|
332
|
+
workers can run in parallel against a single shared container, which listens
|
|
333
|
+
on port 8001. Use Node 24, the version CI runs.
|
|
127
334
|
|
|
128
335
|
```bash
|
|
129
336
|
pnpm install
|
|
@@ -141,6 +348,29 @@ reachable. Override the host/port with `PAYLOAD_DDB_TEST_HOST` /
|
|
|
141
348
|
See [`test/README.md`](./test/README.md) for the harness layout and how to
|
|
142
349
|
add a new spec.
|
|
143
350
|
|
|
351
|
+
### Releasing
|
|
352
|
+
|
|
353
|
+
Releases are cut by CI on every push to `main` that carries a
|
|
354
|
+
[changeset](https://github.com/changesets/changesets) (`pnpm changeset`):
|
|
355
|
+
|
|
356
|
+
1. The integration tests run.
|
|
357
|
+
2. The version is bumped, committed as `chore(release): vX.Y.Z` and tagged.
|
|
358
|
+
3. The package is published to GitHub Packages as
|
|
359
|
+
`@aih-software/payload-ddb`, and a GitHub Release is created.
|
|
360
|
+
4. For minor and major bumps, the same build is published to the public npm
|
|
361
|
+
registry as `@open-aih/payload-ddb`.
|
|
362
|
+
|
|
363
|
+
The public publish uses npm
|
|
364
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers), so there is
|
|
365
|
+
no npm token to rotate. The trusted publisher configured on npmjs.com must
|
|
366
|
+
name this repository and the workflow file `release.yml`.
|
|
367
|
+
|
|
368
|
+
**Prereleases.** To try a change in a real project before releasing it, push
|
|
369
|
+
the commit to the `beta` branch (`git push origin HEAD:beta --force`). The
|
|
370
|
+
same workflow publishes it to both registries as `X.Y.Z-beta.<timestamp>`
|
|
371
|
+
under the `beta` dist-tag — install it with `@open-aih/payload-ddb@beta` —
|
|
372
|
+
and commits, tags and merges nothing. `latest` is untouched.
|
|
373
|
+
|
|
144
374
|
## License
|
|
145
375
|
|
|
146
376
|
MIT
|
package/dist/count.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"count.d.ts","sourceRoot":"","sources":["../src/count.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"count.d.ts","sourceRoot":"","sources":["../src/count.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAOpC,eAAO,MAAM,KAAK,EAAE,KAInB,CAAA"}
|
package/dist/count.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { prepareCollectionWhere } from './utilities/prepareWhere.js';
|
|
1
2
|
import { queryCount } from './utilities/queryCount.js';
|
|
2
|
-
export const count = async function count({ collection, where }) {
|
|
3
|
-
const
|
|
3
|
+
export const count = async function count({ collection, locale, where }) {
|
|
4
|
+
const cast = await prepareCollectionWhere(this, collection, where, locale);
|
|
5
|
+
const totalDocs = await queryCount(this, this.resolvePartition(collection), cast);
|
|
4
6
|
return {
|
|
5
7
|
totalDocs
|
|
6
8
|
};
|
package/dist/count.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/count.ts"],"sourcesContent":["import type { Count } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { queryCount } from './utilities/queryCount.js'\n\nexport const count: Count = async function count(this: DynamoAdapter, { collection, where }) {\n const totalDocs = await queryCount(this, this.resolvePartition(collection),
|
|
1
|
+
{"version":3,"sources":["../src/count.ts"],"sourcesContent":["import type { Count } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { prepareCollectionWhere } from './utilities/prepareWhere.js'\nimport { queryCount } from './utilities/queryCount.js'\n\nexport const count: Count = async function count(this: DynamoAdapter, { collection, locale, where }) {\n const cast = await prepareCollectionWhere(this, collection, where, locale)\n const totalDocs = await queryCount(this, this.resolvePartition(collection), cast)\n return { totalDocs }\n}\n"],"names":["prepareCollectionWhere","queryCount","count","collection","locale","where","cast","totalDocs","resolvePartition"],"mappings":"AAIA,SAASA,sBAAsB,QAAQ,8BAA6B;AACpE,SAASC,UAAU,QAAQ,4BAA2B;AAEtD,OAAO,MAAMC,QAAe,eAAeA,MAA2B,EAAEC,UAAU,EAAEC,MAAM,EAAEC,KAAK,EAAE;IACjG,MAAMC,OAAO,MAAMN,uBAAuB,IAAI,EAAEG,YAAYE,OAAOD;IACnE,MAAMG,YAAY,MAAMN,WAAW,IAAI,EAAE,IAAI,CAACO,gBAAgB,CAACL,aAAaG;IAC5E,OAAO;QAAEC;IAAU;AACrB,EAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"countGlobalVersions.d.ts","sourceRoot":"","sources":["../src/countGlobalVersions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"countGlobalVersions.d.ts","sourceRoot":"","sources":["../src/countGlobalVersions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAOlD,eAAO,MAAM,mBAAmB,EAAE,mBAOjC,CAAA"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { prepareVersionsWhere } from './utilities/prepareWhere.js';
|
|
1
2
|
import { queryCount } from './utilities/queryCount.js';
|
|
2
|
-
export const countGlobalVersions = async function countGlobalVersions({ global, where }) {
|
|
3
|
+
export const countGlobalVersions = async function countGlobalVersions({ global, locale, where: rawWhere }) {
|
|
4
|
+
const where = await prepareVersionsWhere(this, {
|
|
5
|
+
kind: 'global',
|
|
6
|
+
slug: global
|
|
7
|
+
}, rawWhere, locale);
|
|
3
8
|
const totalDocs = await queryCount(this, this.resolveVersionsPartition(global), where);
|
|
4
9
|
return {
|
|
5
10
|
totalDocs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/countGlobalVersions.ts"],"sourcesContent":["import type { CountGlobalVersions } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { queryCount } from './utilities/queryCount.js'\n\nexport const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(\n this: DynamoAdapter,\n { global, where },\n) {\n const totalDocs = await queryCount(this, this.resolveVersionsPartition(global), where)\n return { totalDocs }\n}\n"],"names":["queryCount","countGlobalVersions","global","where","totalDocs","resolveVersionsPartition"],"mappings":"AAIA,SAASA,UAAU,QAAQ,4BAA2B;AAEtD,OAAO,MAAMC,sBAA2C,eAAeA,oBAErE,EAAEC,MAAM,EAAEC,
|
|
1
|
+
{"version":3,"sources":["../src/countGlobalVersions.ts"],"sourcesContent":["import type { CountGlobalVersions } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { prepareVersionsWhere } from './utilities/prepareWhere.js'\nimport { queryCount } from './utilities/queryCount.js'\n\nexport const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(\n this: DynamoAdapter,\n { global, locale, where: rawWhere },\n) {\n const where = await prepareVersionsWhere(this, { kind: 'global', slug: global }, rawWhere, locale)\n const totalDocs = await queryCount(this, this.resolveVersionsPartition(global), where)\n return { totalDocs }\n}\n"],"names":["prepareVersionsWhere","queryCount","countGlobalVersions","global","locale","where","rawWhere","kind","slug","totalDocs","resolveVersionsPartition"],"mappings":"AAIA,SAASA,oBAAoB,QAAQ,8BAA6B;AAClE,SAASC,UAAU,QAAQ,4BAA2B;AAEtD,OAAO,MAAMC,sBAA2C,eAAeA,oBAErE,EAAEC,MAAM,EAAEC,MAAM,EAAEC,OAAOC,QAAQ,EAAE;IAEnC,MAAMD,QAAQ,MAAML,qBAAqB,IAAI,EAAE;QAAEO,MAAM;QAAUC,MAAML;IAAO,GAAGG,UAAUF;IAC3F,MAAMK,YAAY,MAAMR,WAAW,IAAI,EAAE,IAAI,CAACS,wBAAwB,CAACP,SAASE;IAChF,OAAO;QAAEI;IAAU;AACrB,EAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"countVersions.d.ts","sourceRoot":"","sources":["../src/countVersions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"countVersions.d.ts","sourceRoot":"","sources":["../src/countVersions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAO5C,eAAO,MAAM,aAAa,EAAE,aAO3B,CAAA"}
|
package/dist/countVersions.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { prepareVersionsWhere } from './utilities/prepareWhere.js';
|
|
1
2
|
import { queryCount } from './utilities/queryCount.js';
|
|
2
|
-
export const countVersions = async function countVersions({ collection, where }) {
|
|
3
|
+
export const countVersions = async function countVersions({ collection, locale, where: rawWhere }) {
|
|
4
|
+
const where = await prepareVersionsWhere(this, {
|
|
5
|
+
kind: 'collection',
|
|
6
|
+
slug: collection
|
|
7
|
+
}, rawWhere, locale);
|
|
3
8
|
const totalDocs = await queryCount(this, this.resolveVersionsPartition(collection), where);
|
|
4
9
|
return {
|
|
5
10
|
totalDocs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/countVersions.ts"],"sourcesContent":["import type { CountVersions } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { queryCount } from './utilities/queryCount.js'\n\nexport const countVersions: CountVersions = async function countVersions(\n this: DynamoAdapter,\n { collection, where },\n) {\n const totalDocs = await queryCount(this, this.resolveVersionsPartition(collection), where)\n return { totalDocs }\n}\n"],"names":["queryCount","countVersions","collection","where","totalDocs","resolveVersionsPartition"],"mappings":"AAIA,SAASA,UAAU,QAAQ,4BAA2B;AAEtD,OAAO,MAAMC,gBAA+B,eAAeA,cAEzD,EAAEC,UAAU,EAAEC,
|
|
1
|
+
{"version":3,"sources":["../src/countVersions.ts"],"sourcesContent":["import type { CountVersions } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { prepareVersionsWhere } from './utilities/prepareWhere.js'\nimport { queryCount } from './utilities/queryCount.js'\n\nexport const countVersions: CountVersions = async function countVersions(\n this: DynamoAdapter,\n { collection, locale, where: rawWhere },\n) {\n const where = await prepareVersionsWhere(this, { kind: 'collection', slug: collection }, rawWhere, locale)\n const totalDocs = await queryCount(this, this.resolveVersionsPartition(collection), where)\n return { totalDocs }\n}\n"],"names":["prepareVersionsWhere","queryCount","countVersions","collection","locale","where","rawWhere","kind","slug","totalDocs","resolveVersionsPartition"],"mappings":"AAIA,SAASA,oBAAoB,QAAQ,8BAA6B;AAClE,SAASC,UAAU,QAAQ,4BAA2B;AAEtD,OAAO,MAAMC,gBAA+B,eAAeA,cAEzD,EAAEC,UAAU,EAAEC,MAAM,EAAEC,OAAOC,QAAQ,EAAE;IAEvC,MAAMD,QAAQ,MAAML,qBAAqB,IAAI,EAAE;QAAEO,MAAM;QAAcC,MAAML;IAAW,GAAGG,UAAUF;IACnG,MAAMK,YAAY,MAAMR,WAAW,IAAI,EAAE,IAAI,CAACS,wBAAwB,CAACP,aAAaE;IACpF,OAAO;QAAEI;IAAU;AACrB,EAAC"}
|
package/dist/create.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAWrC,eAAO,MAAM,MAAM,EAAE,MA+CpB,CAAA"}
|
package/dist/create.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
2
|
+
import { backoff, conflictError, getById, MAX_WRITE_ATTEMPTS } from './utilities/optimistic.js';
|
|
4
3
|
import { projectForCollection } from './utilities/resolveSchema.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
9
|
-
}
|
|
4
|
+
import { mustBeUniqueError, rethrowUniqueViolation } from './utilities/writeErrors.js';
|
|
5
|
+
import { isConditionalCheckFailed, putItem } from './utilities/writeItems.js';
|
|
6
|
+
export const create = async function create({ collection, customID, data, req, returning }) {
|
|
10
7
|
const id = customID ?? data['id'] ?? randomUUID();
|
|
11
8
|
const now = new Date().toISOString();
|
|
12
9
|
// Spread `data` first, then nullish-coalesce timestamps. Explicit values
|
|
@@ -24,14 +21,31 @@ export const create = async function create({ collection, customID, data, return
|
|
|
24
21
|
// layer, so without this any stray request-body key (notoriously the
|
|
25
22
|
// registration form's `confirm-password`) would persist verbatim.
|
|
26
23
|
const projected = projectForCollection(this, collection, item);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
const partition = this.resolvePartition(collection);
|
|
25
|
+
for(let attempt = 1;; attempt++){
|
|
26
|
+
try {
|
|
27
|
+
// Insert-only: a plain `PutItem` would replace whatever row already
|
|
28
|
+
// sits at this id, so a caller-supplied id that collides with an
|
|
29
|
+
// existing doc would silently destroy it. `collection` adds the
|
|
30
|
+
// document's unique guards and index rows to the same transaction.
|
|
31
|
+
await putItem(this, partition, projected, {
|
|
32
|
+
collection,
|
|
33
|
+
mustNotExist: true
|
|
34
|
+
});
|
|
35
|
+
break;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
rethrowUniqueViolation(err, collection, req);
|
|
38
|
+
if (!isConditionalCheckFailed(err)) throw err;
|
|
39
|
+
// Either the id is taken, or this write collided with a concurrent one
|
|
40
|
+
// over a unique value. Surface the first the way the Mongo and Postgres
|
|
41
|
+
// adapters surface a duplicate primary key — a `ValidationError` on
|
|
42
|
+
// `id` — and retry the second, which resolves to a clean success or a
|
|
43
|
+
// unique violation.
|
|
44
|
+
if (await getById(this, partition, id)) throw mustBeUniqueError('id', collection, req);
|
|
45
|
+
if (attempt >= MAX_WRITE_ATTEMPTS) throw conflictError();
|
|
46
|
+
await backoff(attempt);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
35
49
|
return returning === false ? null : projected;
|
|
36
50
|
};
|
|
37
51
|
|
package/dist/create.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/create.ts"],"sourcesContent":["import type { Create } from 'payload'\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/create.ts"],"sourcesContent":["import type { Create } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { backoff, conflictError, getById, MAX_WRITE_ATTEMPTS } from './utilities/optimistic.js'\nimport { projectForCollection } from './utilities/resolveSchema.js'\nimport { mustBeUniqueError, rethrowUniqueViolation } from './utilities/writeErrors.js'\nimport { isConditionalCheckFailed, putItem } from './utilities/writeItems.js'\n\nexport const create: Create = async function create(\n this: DynamoAdapter,\n { collection, customID, data, req, returning },\n) {\n const id = customID ?? data['id'] ?? randomUUID()\n const now = new Date().toISOString()\n // Spread `data` first, then nullish-coalesce timestamps. Explicit values\n // in `data` (versions/restore, migrations backdating) still win, but a\n // payload-passed `data.createdAt = undefined` no longer wipes the default\n // back to undefined — which the marshaller would then drop with\n // `removeUndefinedValues: true`, leaving the row without timestamps.\n const item: Record<string, unknown> = {\n ...data,\n id,\n createdAt: data['createdAt'] ?? now,\n updatedAt: data['updatedAt'] ?? now,\n }\n // Project against the collection's declared fields. DDB has no schema\n // layer, so without this any stray request-body key (notoriously the\n // registration form's `confirm-password`) would persist verbatim.\n const projected = projectForCollection(this, collection, item)\n const partition = this.resolvePartition(collection)\n\n for (let attempt = 1; ; attempt++) {\n try {\n // Insert-only: a plain `PutItem` would replace whatever row already\n // sits at this id, so a caller-supplied id that collides with an\n // existing doc would silently destroy it. `collection` adds the\n // document's unique guards and index rows to the same transaction.\n await putItem(this, partition, projected, { collection, mustNotExist: true })\n break\n } catch (err) {\n rethrowUniqueViolation(err, collection, req)\n if (!isConditionalCheckFailed(err)) throw err\n\n // Either the id is taken, or this write collided with a concurrent one\n // over a unique value. Surface the first the way the Mongo and Postgres\n // adapters surface a duplicate primary key — a `ValidationError` on\n // `id` — and retry the second, which resolves to a clean success or a\n // unique violation.\n if (await getById(this, partition, id)) throw mustBeUniqueError('id', collection, req)\n if (attempt >= MAX_WRITE_ATTEMPTS) throw conflictError()\n await backoff(attempt)\n }\n }\n\n return returning === false ? (null as never) : projected\n}\n"],"names":["randomUUID","backoff","conflictError","getById","MAX_WRITE_ATTEMPTS","projectForCollection","mustBeUniqueError","rethrowUniqueViolation","isConditionalCheckFailed","putItem","create","collection","customID","data","req","returning","id","now","Date","toISOString","item","createdAt","updatedAt","projected","partition","resolvePartition","attempt","mustNotExist","err"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AAIxC,SAASC,OAAO,EAAEC,aAAa,EAAEC,OAAO,EAAEC,kBAAkB,QAAQ,4BAA2B;AAC/F,SAASC,oBAAoB,QAAQ,+BAA8B;AACnE,SAASC,iBAAiB,EAAEC,sBAAsB,QAAQ,6BAA4B;AACtF,SAASC,wBAAwB,EAAEC,OAAO,QAAQ,4BAA2B;AAE7E,OAAO,MAAMC,SAAiB,eAAeA,OAE3C,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;IAE9C,MAAMC,KAAKJ,YAAYC,IAAI,CAAC,KAAK,IAAIb;IACrC,MAAMiB,MAAM,IAAIC,OAAOC,WAAW;IAClC,yEAAyE;IACzE,uEAAuE;IACvE,0EAA0E;IAC1E,gEAAgE;IAChE,qEAAqE;IACrE,MAAMC,OAAgC;QACpC,GAAGP,IAAI;QACPG;QACAK,WAAWR,IAAI,CAAC,YAAY,IAAII;QAChCK,WAAWT,IAAI,CAAC,YAAY,IAAII;IAClC;IACA,sEAAsE;IACtE,qEAAqE;IACrE,kEAAkE;IAClE,MAAMM,YAAYlB,qBAAqB,IAAI,EAAEM,YAAYS;IACzD,MAAMI,YAAY,IAAI,CAACC,gBAAgB,CAACd;IAExC,IAAK,IAAIe,UAAU,IAAKA,UAAW;QACjC,IAAI;YACF,oEAAoE;YACpE,iEAAiE;YACjE,gEAAgE;YAChE,mEAAmE;YACnE,MAAMjB,QAAQ,IAAI,EAAEe,WAAWD,WAAW;gBAAEZ;gBAAYgB,cAAc;YAAK;YAC3E;QACF,EAAE,OAAOC,KAAK;YACZrB,uBAAuBqB,KAAKjB,YAAYG;YACxC,IAAI,CAACN,yBAAyBoB,MAAM,MAAMA;YAE1C,uEAAuE;YACvE,wEAAwE;YACxE,oEAAoE;YACpE,sEAAsE;YACtE,oBAAoB;YACpB,IAAI,MAAMzB,QAAQ,IAAI,EAAEqB,WAAWR,KAAK,MAAMV,kBAAkB,MAAMK,YAAYG;YAClF,IAAIY,WAAWtB,oBAAoB,MAAMF;YACzC,MAAMD,QAAQyB;QAChB;IACF;IAEA,OAAOX,cAAc,QAAS,OAAiBQ;AACjD,EAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createGlobal.d.ts","sourceRoot":"","sources":["../src/createGlobal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"createGlobal.d.ts","sourceRoot":"","sources":["../src/createGlobal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAO3C;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,YAU1B,CAAA"}
|
package/dist/createGlobal.js
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
|
-
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
-
import { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
|
|
3
1
|
import { projectForGlobal } from './utilities/resolveSchema.js';
|
|
2
|
+
import { putItem } from './utilities/writeItems.js';
|
|
4
3
|
/**
|
|
5
4
|
* Insert a global's singleton row. The slug is forced as the row's `id` —
|
|
6
5
|
* this is what makes `findGlobal`/`updateGlobal` deterministic single-key
|
|
7
6
|
* reads (`pk = slug`, `sk = slug`).
|
|
8
7
|
*/ export const createGlobal = async function createGlobal({ slug, data, returning }) {
|
|
9
|
-
const docClient = this.docClient;
|
|
10
|
-
if (!docClient) {
|
|
11
|
-
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
12
|
-
}
|
|
13
8
|
const item = {
|
|
14
9
|
...data,
|
|
15
10
|
id: slug
|
|
16
11
|
};
|
|
17
12
|
const projected = projectForGlobal(this, slug, item);
|
|
18
|
-
await
|
|
19
|
-
TableName: this.tableName,
|
|
20
|
-
Item: normalizeForDynamo({
|
|
21
|
-
...projected,
|
|
22
|
-
pk: this.resolvePartition(slug),
|
|
23
|
-
sk: slug
|
|
24
|
-
})
|
|
25
|
-
}));
|
|
13
|
+
await putItem(this, this.resolvePartition(slug), projected);
|
|
26
14
|
return returning === false ? null : projected;
|
|
27
15
|
};
|
|
28
16
|
|
package/dist/createGlobal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/createGlobal.ts"],"sourcesContent":["import type { CreateGlobal } from 'payload'\n\nimport
|
|
1
|
+
{"version":3,"sources":["../src/createGlobal.ts"],"sourcesContent":["import type { CreateGlobal } from 'payload'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { projectForGlobal } from './utilities/resolveSchema.js'\nimport { putItem } from './utilities/writeItems.js'\n\n/**\n * Insert a global's singleton row. The slug is forced as the row's `id` —\n * this is what makes `findGlobal`/`updateGlobal` deterministic single-key\n * reads (`pk = slug`, `sk = slug`).\n */\nexport const createGlobal: CreateGlobal = async function createGlobal(\n this: DynamoAdapter,\n { slug, data, returning },\n) {\n const item: Record<string, unknown> = { ...data, id: slug }\n const projected = projectForGlobal(this, slug, item)\n\n await putItem(this, this.resolvePartition(slug), projected)\n\n return returning === false ? (null as never) : (projected as never)\n}\n"],"names":["projectForGlobal","putItem","createGlobal","slug","data","returning","item","id","projected","resolvePartition"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,+BAA8B;AAC/D,SAASC,OAAO,QAAQ,4BAA2B;AAEnD;;;;CAIC,GACD,OAAO,MAAMC,eAA6B,eAAeA,aAEvD,EAAEC,IAAI,EAAEC,IAAI,EAAEC,SAAS,EAAE;IAEzB,MAAMC,OAAgC;QAAE,GAAGF,IAAI;QAAEG,IAAIJ;IAAK;IAC1D,MAAMK,YAAYR,iBAAiB,IAAI,EAAEG,MAAMG;IAE/C,MAAML,QAAQ,IAAI,EAAE,IAAI,CAACQ,gBAAgB,CAACN,OAAOK;IAEjD,OAAOH,cAAc,QAAS,OAAkBG;AAClD,EAAC"}
|
|
@@ -2,7 +2,7 @@ import type { CreateGlobalVersion } from 'payload';
|
|
|
2
2
|
/**
|
|
3
3
|
* Like `createVersion` but for global singletons: there's no `parent`, and
|
|
4
4
|
* the "latest" scope is the entire global's versions partition. Same atomic
|
|
5
|
-
* flip+put via `
|
|
5
|
+
* flip+put via `putVersionAsLatest`. See `createVersion` for rationale.
|
|
6
6
|
*/
|
|
7
7
|
export declare const createGlobalVersion: CreateGlobalVersion;
|
|
8
8
|
//# sourceMappingURL=createGlobalVersion.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createGlobalVersion.d.ts","sourceRoot":"","sources":["../src/createGlobalVersion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAWlD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"createGlobalVersion.d.ts","sourceRoot":"","sources":["../src/createGlobalVersion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAWlD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,mBA8CjC,CAAA"}
|