@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.
Files changed (196) hide show
  1. package/README.md +256 -26
  2. package/dist/count.d.ts.map +1 -1
  3. package/dist/count.js +4 -2
  4. package/dist/count.js.map +1 -1
  5. package/dist/countGlobalVersions.d.ts.map +1 -1
  6. package/dist/countGlobalVersions.js +6 -1
  7. package/dist/countGlobalVersions.js.map +1 -1
  8. package/dist/countVersions.d.ts.map +1 -1
  9. package/dist/countVersions.js +6 -1
  10. package/dist/countVersions.js.map +1 -1
  11. package/dist/create.d.ts.map +1 -1
  12. package/dist/create.js +29 -15
  13. package/dist/create.js.map +1 -1
  14. package/dist/createGlobal.d.ts.map +1 -1
  15. package/dist/createGlobal.js +2 -14
  16. package/dist/createGlobal.js.map +1 -1
  17. package/dist/createGlobalVersion.d.ts +1 -1
  18. package/dist/createGlobalVersion.d.ts.map +1 -1
  19. package/dist/createGlobalVersion.js +15 -49
  20. package/dist/createGlobalVersion.js.map +1 -1
  21. package/dist/createVersion.d.ts +11 -7
  22. package/dist/createVersion.d.ts.map +1 -1
  23. package/dist/createVersion.js +24 -67
  24. package/dist/createVersion.js.map +1 -1
  25. package/dist/deleteMany.d.ts +1 -6
  26. package/dist/deleteMany.d.ts.map +1 -1
  27. package/dist/deleteMany.js +11 -19
  28. package/dist/deleteMany.js.map +1 -1
  29. package/dist/deleteOne.d.ts.map +1 -1
  30. package/dist/deleteOne.js +20 -24
  31. package/dist/deleteOne.js.map +1 -1
  32. package/dist/deleteVersions.d.ts.map +1 -1
  33. package/dist/deleteVersions.js +12 -14
  34. package/dist/deleteVersions.js.map +1 -1
  35. package/dist/find.d.ts +16 -13
  36. package/dist/find.d.ts.map +1 -1
  37. package/dist/find.js +97 -23
  38. package/dist/find.js.map +1 -1
  39. package/dist/findDistinct.d.ts.map +1 -1
  40. package/dist/findDistinct.js +3 -1
  41. package/dist/findDistinct.js.map +1 -1
  42. package/dist/findGlobal.d.ts.map +1 -1
  43. package/dist/findGlobal.js +3 -2
  44. package/dist/findGlobal.js.map +1 -1
  45. package/dist/findGlobalVersions.d.ts.map +1 -1
  46. package/dist/findGlobalVersions.js +10 -2
  47. package/dist/findGlobalVersions.js.map +1 -1
  48. package/dist/findOne.d.ts.map +1 -1
  49. package/dist/findOne.js +17 -6
  50. package/dist/findOne.js.map +1 -1
  51. package/dist/findVersions.d.ts.map +1 -1
  52. package/dist/findVersions.js +10 -2
  53. package/dist/findVersions.js.map +1 -1
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +8 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/init.d.ts.map +1 -1
  59. package/dist/init.js +5 -0
  60. package/dist/init.js.map +1 -1
  61. package/dist/queryDrafts.d.ts.map +1 -1
  62. package/dist/queryDrafts.js +29 -10
  63. package/dist/queryDrafts.js.map +1 -1
  64. package/dist/types.d.ts +14 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/updateGlobal.d.ts +5 -4
  68. package/dist/updateGlobal.d.ts.map +1 -1
  69. package/dist/updateGlobal.js +22 -36
  70. package/dist/updateGlobal.js.map +1 -1
  71. package/dist/updateGlobalVersion.d.ts.map +1 -1
  72. package/dist/updateGlobalVersion.js +3 -49
  73. package/dist/updateGlobalVersion.js.map +1 -1
  74. package/dist/updateJobs.d.ts +21 -0
  75. package/dist/updateJobs.d.ts.map +1 -0
  76. package/dist/updateJobs.js +83 -0
  77. package/dist/updateJobs.js.map +1 -0
  78. package/dist/updateMany.d.ts +9 -3
  79. package/dist/updateMany.d.ts.map +1 -1
  80. package/dist/updateMany.js +45 -30
  81. package/dist/updateMany.js.map +1 -1
  82. package/dist/updateOne.d.ts +6 -1
  83. package/dist/updateOne.d.ts.map +1 -1
  84. package/dist/updateOne.js +47 -49
  85. package/dist/updateOne.js.map +1 -1
  86. package/dist/updateVersion.d.ts.map +1 -1
  87. package/dist/updateVersion.js +3 -52
  88. package/dist/updateVersion.js.map +1 -1
  89. package/dist/upsert.d.ts +6 -0
  90. package/dist/upsert.d.ts.map +1 -1
  91. package/dist/upsert.js +55 -41
  92. package/dist/upsert.js.map +1 -1
  93. package/dist/utilities/applySorts.d.ts +5 -2
  94. package/dist/utilities/applySorts.d.ts.map +1 -1
  95. package/dist/utilities/applySorts.js +11 -6
  96. package/dist/utilities/applySorts.js.map +1 -1
  97. package/dist/utilities/backfillIndexes.d.ts +71 -0
  98. package/dist/utilities/backfillIndexes.d.ts.map +1 -0
  99. package/dist/utilities/backfillIndexes.js +149 -0
  100. package/dist/utilities/backfillIndexes.js.map +1 -0
  101. package/dist/utilities/batchGet.d.ts +15 -0
  102. package/dist/utilities/batchGet.d.ts.map +1 -0
  103. package/dist/utilities/batchGet.js +56 -0
  104. package/dist/utilities/batchGet.js.map +1 -0
  105. package/dist/utilities/buildFilterExpression.d.ts +29 -14
  106. package/dist/utilities/buildFilterExpression.d.ts.map +1 -1
  107. package/dist/utilities/buildFilterExpression.js +55 -14
  108. package/dist/utilities/buildFilterExpression.js.map +1 -1
  109. package/dist/utilities/castWhere.d.ts +84 -0
  110. package/dist/utilities/castWhere.d.ts.map +1 -0
  111. package/dist/utilities/castWhere.js +254 -0
  112. package/dist/utilities/castWhere.js.map +1 -0
  113. package/dist/utilities/draftsFallback.d.ts.map +1 -1
  114. package/dist/utilities/draftsFallback.js +3 -6
  115. package/dist/utilities/draftsFallback.js.map +1 -1
  116. package/dist/utilities/fieldIndexes.d.ts +130 -0
  117. package/dist/utilities/fieldIndexes.d.ts.map +1 -0
  118. package/dist/utilities/fieldIndexes.js +286 -0
  119. package/dist/utilities/fieldIndexes.js.map +1 -0
  120. package/dist/utilities/findFirst.d.ts +5 -2
  121. package/dist/utilities/findFirst.d.ts.map +1 -1
  122. package/dist/utilities/findFirst.js +19 -46
  123. package/dist/utilities/findFirst.js.map +1 -1
  124. package/dist/utilities/geo.d.ts +36 -0
  125. package/dist/utilities/geo.d.ts.map +1 -0
  126. package/dist/utilities/geo.js +108 -0
  127. package/dist/utilities/geo.js.map +1 -0
  128. package/dist/utilities/indexLookup.d.ts +73 -0
  129. package/dist/utilities/indexLookup.d.ts.map +1 -0
  130. package/dist/utilities/indexLookup.js +361 -0
  131. package/dist/utilities/indexLookup.js.map +1 -0
  132. package/dist/utilities/indexState.d.ts +13 -0
  133. package/dist/utilities/indexState.d.ts.map +1 -0
  134. package/dist/utilities/indexState.js +199 -0
  135. package/dist/utilities/indexState.js.map +1 -0
  136. package/dist/utilities/mapWithConcurrency.d.ts +12 -0
  137. package/dist/utilities/mapWithConcurrency.d.ts.map +1 -0
  138. package/dist/utilities/mapWithConcurrency.js +32 -0
  139. package/dist/utilities/mapWithConcurrency.js.map +1 -0
  140. package/dist/utilities/matchesWhere.d.ts +25 -5
  141. package/dist/utilities/matchesWhere.d.ts.map +1 -1
  142. package/dist/utilities/matchesWhere.js +150 -38
  143. package/dist/utilities/matchesWhere.js.map +1 -1
  144. package/dist/utilities/operators.d.ts +22 -12
  145. package/dist/utilities/operators.d.ts.map +1 -1
  146. package/dist/utilities/operators.js +36 -31
  147. package/dist/utilities/operators.js.map +1 -1
  148. package/dist/utilities/optimistic.d.ts +29 -0
  149. package/dist/utilities/optimistic.d.ts.map +1 -0
  150. package/dist/utilities/optimistic.js +58 -0
  151. package/dist/utilities/optimistic.js.map +1 -0
  152. package/dist/utilities/prepareWhere.d.ts +63 -0
  153. package/dist/utilities/prepareWhere.d.ts.map +1 -0
  154. package/dist/utilities/prepareWhere.js +198 -0
  155. package/dist/utilities/prepareWhere.js.map +1 -0
  156. package/dist/utilities/queryCount.d.ts +14 -4
  157. package/dist/utilities/queryCount.d.ts.map +1 -1
  158. package/dist/utilities/queryCount.js +23 -46
  159. package/dist/utilities/queryCount.js.map +1 -1
  160. package/dist/utilities/queryMatching.d.ts +18 -10
  161. package/dist/utilities/queryMatching.d.ts.map +1 -1
  162. package/dist/utilities/queryMatching.js +26 -57
  163. package/dist/utilities/queryMatching.js.map +1 -1
  164. package/dist/utilities/queryPartition.d.ts +26 -0
  165. package/dist/utilities/queryPartition.d.ts.map +1 -0
  166. package/dist/utilities/queryPartition.js +55 -0
  167. package/dist/utilities/queryPartition.js.map +1 -0
  168. package/dist/utilities/resolveJoins.d.ts +37 -0
  169. package/dist/utilities/resolveJoins.d.ts.map +1 -0
  170. package/dist/utilities/resolveJoins.js +195 -0
  171. package/dist/utilities/resolveJoins.js.map +1 -0
  172. package/dist/utilities/stripInternalKeys.d.ts +17 -3
  173. package/dist/utilities/stripInternalKeys.d.ts.map +1 -1
  174. package/dist/utilities/stripInternalKeys.js +25 -4
  175. package/dist/utilities/stripInternalKeys.js.map +1 -1
  176. package/dist/utilities/uniqueGuards.d.ts +73 -0
  177. package/dist/utilities/uniqueGuards.d.ts.map +1 -0
  178. package/dist/utilities/uniqueGuards.js +132 -0
  179. package/dist/utilities/uniqueGuards.js.map +1 -0
  180. package/dist/utilities/updateVersionRow.d.ts +19 -0
  181. package/dist/utilities/updateVersionRow.d.ts.map +1 -0
  182. package/dist/utilities/updateVersionRow.js +41 -0
  183. package/dist/utilities/updateVersionRow.js.map +1 -0
  184. package/dist/utilities/versionIndex.d.ts +45 -0
  185. package/dist/utilities/versionIndex.d.ts.map +1 -0
  186. package/dist/utilities/versionIndex.js +103 -0
  187. package/dist/utilities/versionIndex.js.map +1 -0
  188. package/dist/utilities/writeErrors.d.ts +11 -0
  189. package/dist/utilities/writeErrors.d.ts.map +1 -0
  190. package/dist/utilities/writeErrors.js +27 -0
  191. package/dist/utilities/writeErrors.js.map +1 -0
  192. package/dist/utilities/writeItems.d.ts +167 -0
  193. package/dist/utilities/writeItems.d.ts.map +1 -0
  194. package/dist/utilities/writeItems.js +675 -0
  195. package/dist/utilities/writeItems.js.map +1 -0
  196. 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. The
7
- > common access patterns (CRUD, drafts, versions, globals, locales-as-fields)
8
- > work today. The current strategy is "correct first, optimize later" — single
9
- > shared table with a `Query`-per-collection access pattern, no GSIs, no range
10
- > operators in `FilterExpression`. Suitable for development and
11
- > small-to-medium workloads; large collections will want the optimization
12
- > milestones tracked in the issues.
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
- - **No GSIs or LSIs in v2.** Per-collection reads are `Query`s scoped by `pk`;
92
- predicates beyond `pk` get pushed to `FilterExpression` (DynamoDB still
93
- reads every row in the partition before filtering).
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 `queryDrafts`.
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
- Supported `where` operators: `equals`, `not_equals`, `greater_than`,
103
- `greater_than_equal`, `less_than`, `less_than_equal`, `in`, `not_in`, `exists`,
104
- `like`, plus `and` / `or` composition. Anything else throws by design so
105
- coverage gaps surface loudly. `like` is case-insensitive and evaluated in JS
106
- (DynamoDB's `contains()` is case-sensitive, so the adapter doesn't push it
107
- down).
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
- - **No GSI routing.** Every non-id read is a `Query` against the collection's
112
- partition (with `FilterExpression` pushdown — DynamoDB still reads every
113
- row in the partition internally, but only matching rows come back over the
114
- wire). Adding GSIs that mirror Payload's common access patterns
115
- (e.g. `email` for auth, `slug` for public-facing collections) is the
116
- highest-impact remaining optimization milestone.
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. Also make sure port 8000 is open.
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
@@ -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;AAMpC,eAAO,MAAM,KAAK,EAAE,KAGnB,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 totalDocs = await queryCount(this, this.resolvePartition(collection), where);
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), where)\n return { totalDocs }\n}\n"],"names":["queryCount","count","collection","where","totalDocs","resolvePartition"],"mappings":"AAIA,SAASA,UAAU,QAAQ,4BAA2B;AAEtD,OAAO,MAAMC,QAAe,eAAeA,MAA2B,EAAEC,UAAU,EAAEC,KAAK,EAAE;IACzF,MAAMC,YAAY,MAAMJ,WAAW,IAAI,EAAE,IAAI,CAACK,gBAAgB,CAACH,aAAaC;IAC5E,OAAO;QAAEC;IAAU;AACrB,EAAC"}
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;AAMlD,eAAO,MAAM,mBAAmB,EAAE,mBAMjC,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,KAAK,EAAE;IAEjB,MAAMC,YAAY,MAAMJ,WAAW,IAAI,EAAE,IAAI,CAACK,wBAAwB,CAACH,SAASC;IAChF,OAAO;QAAEC;IAAU;AACrB,EAAC"}
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;AAM5C,eAAO,MAAM,aAAa,EAAE,aAM3B,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"}
@@ -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,KAAK,EAAE;IAErB,MAAMC,YAAY,MAAMJ,WAAW,IAAI,EAAE,IAAI,CAACK,wBAAwB,CAACH,aAAaC;IACpF,OAAO;QAAEC;IAAU;AACrB,EAAC"}
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"}
@@ -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;AAUrC,eAAO,MAAM,MAAM,EAAE,MAuCpB,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 { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
2
+ import { backoff, conflictError, getById, MAX_WRITE_ATTEMPTS } from './utilities/optimistic.js';
4
3
  import { projectForCollection } from './utilities/resolveSchema.js';
5
- export const create = async function create({ collection, customID, data, returning }) {
6
- const docClient = this.docClient;
7
- if (!docClient) {
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
- await docClient.send(new PutCommand({
28
- TableName: this.tableName,
29
- Item: normalizeForDynamo({
30
- ...projected,
31
- pk: this.resolvePartition(collection),
32
- sk: String(id)
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
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/create.ts"],"sourcesContent":["import type { Create } from 'payload'\n\nimport { PutCommand } from '@aws-sdk/lib-dynamodb'\nimport { randomUUID } from 'node:crypto'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { projectForCollection } from './utilities/resolveSchema.js'\n\nexport const create: Create = async function create(\n this: DynamoAdapter,\n { collection, customID, data, returning },\n) {\n const docClient = this.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\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\n await docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: normalizeForDynamo({\n ...projected,\n pk: this.resolvePartition(collection),\n sk: String(id),\n }),\n }),\n )\n\n return returning === false ? (null as never) : projected\n}\n"],"names":["PutCommand","randomUUID","normalizeForDynamo","projectForCollection","create","collection","customID","data","returning","docClient","Error","id","now","Date","toISOString","item","createdAt","updatedAt","projected","send","TableName","tableName","Item","pk","resolvePartition","sk","String"],"mappings":"AAEA,SAASA,UAAU,QAAQ,wBAAuB;AAClD,SAASC,UAAU,QAAQ,cAAa;AAIxC,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,oBAAoB,QAAQ,+BAA8B;AAEnE,OAAO,MAAMC,SAAiB,eAAeA,OAE3C,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,IAAI,EAAEC,SAAS,EAAE;IAEzC,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,KAAKL,YAAYC,IAAI,CAAC,KAAK,IAAIN;IACrC,MAAMW,MAAM,IAAIC,OAAOC,WAAW;IAClC,yEAAyE;IACzE,uEAAuE;IACvE,0EAA0E;IAC1E,gEAAgE;IAChE,qEAAqE;IACrE,MAAMC,OAAgC;QACpC,GAAGR,IAAI;QACPI;QACAK,WAAWT,IAAI,CAAC,YAAY,IAAIK;QAChCK,WAAWV,IAAI,CAAC,YAAY,IAAIK;IAClC;IACA,sEAAsE;IACtE,qEAAqE;IACrE,kEAAkE;IAClE,MAAMM,YAAYf,qBAAqB,IAAI,EAAEE,YAAYU;IAEzD,MAAMN,UAAUU,IAAI,CAClB,IAAInB,WAAW;QACboB,WAAW,IAAI,CAACC,SAAS;QACzBC,MAAMpB,mBAAmB;YACvB,GAAGgB,SAAS;YACZK,IAAI,IAAI,CAACC,gBAAgB,CAACnB;YAC1BoB,IAAIC,OAAOf;QACb;IACF;IAGF,OAAOH,cAAc,QAAS,OAAiBU;AACjD,EAAC"}
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;AAS3C;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,YAwB1B,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"}
@@ -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 docClient.send(new PutCommand({
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
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/createGlobal.ts"],"sourcesContent":["import type { CreateGlobal } from 'payload'\n\nimport { PutCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { projectForGlobal } from './utilities/resolveSchema.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 docClient = this.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const item: Record<string, unknown> = { ...data, id: slug }\n const projected = projectForGlobal(this, slug, item)\n\n await docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: normalizeForDynamo({\n ...projected,\n pk: this.resolvePartition(slug),\n sk: slug,\n }),\n }),\n )\n\n return returning === false ? (null as never) : (projected as never)\n}\n"],"names":["PutCommand","normalizeForDynamo","projectForGlobal","createGlobal","slug","data","returning","docClient","Error","item","id","projected","send","TableName","tableName","Item","pk","resolvePartition","sk"],"mappings":"AAEA,SAASA,UAAU,QAAQ,wBAAuB;AAIlD,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,gBAAgB,QAAQ,+BAA8B;AAE/D;;;;CAIC,GACD,OAAO,MAAMC,eAA6B,eAAeA,aAEvD,EAAEC,IAAI,EAAEC,IAAI,EAAEC,SAAS,EAAE;IAEzB,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,OAAgC;QAAE,GAAGJ,IAAI;QAAEK,IAAIN;IAAK;IAC1D,MAAMO,YAAYT,iBAAiB,IAAI,EAAEE,MAAMK;IAE/C,MAAMF,UAAUK,IAAI,CAClB,IAAIZ,WAAW;QACba,WAAW,IAAI,CAACC,SAAS;QACzBC,MAAMd,mBAAmB;YACvB,GAAGU,SAAS;YACZK,IAAI,IAAI,CAACC,gBAAgB,CAACb;YAC1Bc,IAAId;QACN;IACF;IAGF,OAAOE,cAAc,QAAS,OAAkBK;AAClD,EAAC"}
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 `TransactWriteItems`. See `createVersion` for rationale.
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,mBA2EjC,CAAA"}
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"}