@open-aih/payload-ddb 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +141 -18
  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 +31 -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 +3 -41
  20. package/dist/createGlobalVersion.js.map +1 -1
  21. package/dist/createVersion.d.ts +6 -5
  22. package/dist/createVersion.d.ts.map +1 -1
  23. package/dist/createVersion.js +8 -48
  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 +8 -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 +8 -14
  34. package/dist/deleteVersions.js.map +1 -1
  35. package/dist/find.d.ts.map +1 -1
  36. package/dist/find.js +15 -2
  37. package/dist/find.js.map +1 -1
  38. package/dist/findDistinct.d.ts.map +1 -1
  39. package/dist/findDistinct.js +3 -1
  40. package/dist/findDistinct.js.map +1 -1
  41. package/dist/findGlobal.d.ts.map +1 -1
  42. package/dist/findGlobal.js +3 -2
  43. package/dist/findGlobal.js.map +1 -1
  44. package/dist/findGlobalVersions.d.ts.map +1 -1
  45. package/dist/findGlobalVersions.js +10 -2
  46. package/dist/findGlobalVersions.js.map +1 -1
  47. package/dist/findOne.d.ts.map +1 -1
  48. package/dist/findOne.js +17 -6
  49. package/dist/findOne.js.map +1 -1
  50. package/dist/findVersions.d.ts.map +1 -1
  51. package/dist/findVersions.js +10 -2
  52. package/dist/findVersions.js.map +1 -1
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +7 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/queryDrafts.d.ts.map +1 -1
  58. package/dist/queryDrafts.js +24 -4
  59. package/dist/queryDrafts.js.map +1 -1
  60. package/dist/updateGlobal.d.ts +5 -4
  61. package/dist/updateGlobal.d.ts.map +1 -1
  62. package/dist/updateGlobal.js +22 -36
  63. package/dist/updateGlobal.js.map +1 -1
  64. package/dist/updateGlobalVersion.d.ts.map +1 -1
  65. package/dist/updateGlobalVersion.js +3 -49
  66. package/dist/updateGlobalVersion.js.map +1 -1
  67. package/dist/updateJobs.d.ts +21 -0
  68. package/dist/updateJobs.d.ts.map +1 -0
  69. package/dist/updateJobs.js +83 -0
  70. package/dist/updateJobs.js.map +1 -0
  71. package/dist/updateMany.d.ts +9 -3
  72. package/dist/updateMany.d.ts.map +1 -1
  73. package/dist/updateMany.js +47 -30
  74. package/dist/updateMany.js.map +1 -1
  75. package/dist/updateOne.d.ts +6 -1
  76. package/dist/updateOne.d.ts.map +1 -1
  77. package/dist/updateOne.js +49 -49
  78. package/dist/updateOne.js.map +1 -1
  79. package/dist/updateVersion.d.ts.map +1 -1
  80. package/dist/updateVersion.js +3 -52
  81. package/dist/updateVersion.js.map +1 -1
  82. package/dist/upsert.d.ts +6 -0
  83. package/dist/upsert.d.ts.map +1 -1
  84. package/dist/upsert.js +57 -41
  85. package/dist/upsert.js.map +1 -1
  86. package/dist/utilities/backfillUniqueGuards.d.ts +42 -0
  87. package/dist/utilities/backfillUniqueGuards.d.ts.map +1 -0
  88. package/dist/utilities/backfillUniqueGuards.js +39 -0
  89. package/dist/utilities/backfillUniqueGuards.js.map +1 -0
  90. package/dist/utilities/buildFilterExpression.d.ts +29 -14
  91. package/dist/utilities/buildFilterExpression.d.ts.map +1 -1
  92. package/dist/utilities/buildFilterExpression.js +55 -14
  93. package/dist/utilities/buildFilterExpression.js.map +1 -1
  94. package/dist/utilities/castWhere.d.ts +84 -0
  95. package/dist/utilities/castWhere.d.ts.map +1 -0
  96. package/dist/utilities/castWhere.js +254 -0
  97. package/dist/utilities/castWhere.js.map +1 -0
  98. package/dist/utilities/findFirst.d.ts +3 -2
  99. package/dist/utilities/findFirst.d.ts.map +1 -1
  100. package/dist/utilities/findFirst.js +14 -46
  101. package/dist/utilities/findFirst.js.map +1 -1
  102. package/dist/utilities/geo.d.ts +36 -0
  103. package/dist/utilities/geo.d.ts.map +1 -0
  104. package/dist/utilities/geo.js +108 -0
  105. package/dist/utilities/geo.js.map +1 -0
  106. package/dist/utilities/mapWithConcurrency.d.ts +12 -0
  107. package/dist/utilities/mapWithConcurrency.d.ts.map +1 -0
  108. package/dist/utilities/mapWithConcurrency.js +32 -0
  109. package/dist/utilities/mapWithConcurrency.js.map +1 -0
  110. package/dist/utilities/matchesWhere.d.ts +25 -5
  111. package/dist/utilities/matchesWhere.d.ts.map +1 -1
  112. package/dist/utilities/matchesWhere.js +150 -38
  113. package/dist/utilities/matchesWhere.js.map +1 -1
  114. package/dist/utilities/operators.d.ts +22 -12
  115. package/dist/utilities/operators.d.ts.map +1 -1
  116. package/dist/utilities/operators.js +36 -31
  117. package/dist/utilities/operators.js.map +1 -1
  118. package/dist/utilities/optimistic.d.ts +29 -0
  119. package/dist/utilities/optimistic.d.ts.map +1 -0
  120. package/dist/utilities/optimistic.js +58 -0
  121. package/dist/utilities/optimistic.js.map +1 -0
  122. package/dist/utilities/prepareWhere.d.ts +63 -0
  123. package/dist/utilities/prepareWhere.d.ts.map +1 -0
  124. package/dist/utilities/prepareWhere.js +198 -0
  125. package/dist/utilities/prepareWhere.js.map +1 -0
  126. package/dist/utilities/queryCount.d.ts +6 -3
  127. package/dist/utilities/queryCount.d.ts.map +1 -1
  128. package/dist/utilities/queryCount.js +16 -45
  129. package/dist/utilities/queryCount.js.map +1 -1
  130. package/dist/utilities/queryMatching.d.ts +8 -7
  131. package/dist/utilities/queryMatching.d.ts.map +1 -1
  132. package/dist/utilities/queryMatching.js +17 -54
  133. package/dist/utilities/queryMatching.js.map +1 -1
  134. package/dist/utilities/queryPartition.d.ts +26 -0
  135. package/dist/utilities/queryPartition.d.ts.map +1 -0
  136. package/dist/utilities/queryPartition.js +55 -0
  137. package/dist/utilities/queryPartition.js.map +1 -0
  138. package/dist/utilities/resolveJoins.d.ts +37 -0
  139. package/dist/utilities/resolveJoins.d.ts.map +1 -0
  140. package/dist/utilities/resolveJoins.js +195 -0
  141. package/dist/utilities/resolveJoins.js.map +1 -0
  142. package/dist/utilities/stripInternalKeys.d.ts +17 -3
  143. package/dist/utilities/stripInternalKeys.d.ts.map +1 -1
  144. package/dist/utilities/stripInternalKeys.js +25 -4
  145. package/dist/utilities/stripInternalKeys.js.map +1 -1
  146. package/dist/utilities/uniqueGuards.d.ts +39 -0
  147. package/dist/utilities/uniqueGuards.d.ts.map +1 -0
  148. package/dist/utilities/uniqueGuards.js +106 -0
  149. package/dist/utilities/uniqueGuards.js.map +1 -0
  150. package/dist/utilities/updateVersionRow.d.ts +19 -0
  151. package/dist/utilities/updateVersionRow.d.ts.map +1 -0
  152. package/dist/utilities/updateVersionRow.js +40 -0
  153. package/dist/utilities/updateVersionRow.js.map +1 -0
  154. package/dist/utilities/writeErrors.d.ts +11 -0
  155. package/dist/utilities/writeErrors.d.ts.map +1 -0
  156. package/dist/utilities/writeErrors.js +27 -0
  157. package/dist/utilities/writeErrors.js.map +1 -0
  158. package/dist/utilities/writeItems.d.ts +124 -0
  159. package/dist/utilities/writeItems.d.ts.map +1 -0
  160. package/dist/utilities/writeItems.js +383 -0
  161. package/dist/utilities/writeItems.js.map +1 -0
  162. package/package.json +21 -14
package/README.md CHANGED
@@ -3,22 +3,35 @@
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 with a `Query`-per-collection access pattern
10
+ > and no GSIs. Suitable for development and small-to-medium workloads; large
11
+ > collections will want the optimizations listed under *Known limits*.
12
+
13
+ ## Requirements
14
+
15
+ - **Payload 3.x.** The peer range is `payload >=3.0.0`; the test suite runs
16
+ against 3.93.
17
+ - **Node.js as your Payload version requires.** Payload 3.92 and later need
18
+ Node 22.23.4+ or 24.22.1+. This repo's CI runs on Node 24.
13
19
 
14
20
  ## Installation
15
21
 
22
+ The package is published under two names with the same contents:
23
+
16
24
  ```bash
25
+ # Public npm registry — minor and major releases
26
+ pnpm add @open-aih/payload-ddb
27
+
28
+ # GitHub Packages — every release, including patches
17
29
  pnpm add @aih-software/payload-ddb
18
- # or
19
- npm install @aih-software/payload-ddb
20
30
  ```
21
31
 
32
+ The examples below import from `@aih-software/payload-ddb`; use
33
+ `@open-aih/payload-ddb` instead if that is the one you installed.
34
+
22
35
  ## Quick start (DynamoDB Local)
23
36
 
24
37
  ```ts
@@ -75,6 +88,7 @@ value in the same physical table.
75
88
  | `translateConfig` | `TranslateConfig` | `removeUndefinedValues: true`, `convertClassInstanceToMap: true` | Marshalling options forwarded to `DynamoDBDocumentClient.from`. |
76
89
  | `tableName` | `string` | `'payload'` | The single DynamoDB table backing every collection, global, and versions stream. |
77
90
  | `ensureTables` | `boolean` | `false` | When true, auto-creates the table at init if it doesn't exist. Dev-loop convenience; turn off in production. |
91
+ | `migrationDir` | `string` | `'migrations'` | Directory Payload reads and writes migration files in. |
78
92
 
79
93
  ## Data layout
80
94
 
@@ -88,7 +102,12 @@ value in the same physical table.
88
102
  - **Sort key is always String.** Number-typed collection ids are coerced to
89
103
  strings for `sk` while the original numeric `id` attribute is preserved on
90
104
  the row.
91
- - **No GSIs or LSIs in v2.** Per-collection reads are `Query`s scoped by `pk`;
105
+ - **Revision counter.** Every row also carries `_rev`, bumped on each write
106
+ and stripped on read. Updates are conditional on it, so two writers can't
107
+ silently overwrite each other.
108
+ - **Unique guards:** `pk = "<slug>#unique#<field path>"`, `sk = "<value>"`,
109
+ one small row per value of a `unique: true` field. See *Unique fields*.
110
+ - **No GSIs or LSIs.** Per-collection reads are `Query`s scoped by `pk`;
92
111
  predicates beyond `pk` get pushed to `FilterExpression` (DynamoDB still
93
112
  reads every row in the partition before filtering).
94
113
 
@@ -97,14 +116,91 @@ value in the same physical table.
97
116
  All `BaseDatabaseAdapter` methods: `create`, `find`, `findOne`, `findDistinct`,
98
117
  `count`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `upsert`,
99
118
  `createGlobal`/`findGlobal`/`updateGlobal`, the five collection version methods,
100
- the four global version methods, and `queryDrafts`.
119
+ the four global version methods, `queryDrafts`, and `updateJobs`.
120
+
121
+ ### Queries
122
+
123
+ Every Payload `where` operator is supported: `equals`, `not_equals`,
124
+ `greater_than`, `greater_than_equal`, `less_than`, `less_than_equal`, `in`,
125
+ `not_in`, `all`, `exists`, `like`, `not_like`, `contains`, `near`, `within`,
126
+ `intersects`, plus `and` / `or` composition.
127
+
128
+ - **Pushdown where possible, JS for the rest.** Clauses DynamoDB can express
129
+ go into a `FilterExpression`. The others — the case-insensitive text
130
+ operators (`like`, `not_like`, `contains`), `all`, the geo operators, and
131
+ any path that runs through a list — are evaluated in JS on the rows that
132
+ come back. A `where` that mixes both kinds still narrows the read
133
+ server-side.
134
+ - **Lists behave as in MongoDB.** A `hasMany` field, an `array` or `blocks`
135
+ field, or a list inside `json` is transparent in a path: `tags equals 'x'`
136
+ matches when any element is `'x'`, `items.label equals 'x'` when any row
137
+ has that label. Negative operators hold only when no element matches.
138
+ - **Relationship paths.** `author.name equals 'Ada'` is resolved by querying
139
+ the related collection and filtering on the ids it returns; multi-hop paths
140
+ work the same way.
141
+ - **Localized fields.** With localization enabled, filters and sorts on a
142
+ localized field address the request's locale (`title` → `title.en`), or the
143
+ locale written into the path.
144
+ - **Operand casting.** Operands are cast to the field's stored type, so
145
+ filters sent as strings (REST query strings, admin list filters) behave:
146
+ numeric strings on `number` fields, `'true'`/`'false'` on `checkbox` fields,
147
+ date strings on `date` fields, ids on relationships to numeric-id
148
+ collections, and comma-delimited lists for `in` / `not_in` / `all`.
149
+ DynamoDB compares strictly by type, so an operand the adapter can't cast
150
+ (an unknown path) is compared as given.
151
+ - **`exists`** treats a field stored as `null` the same as a missing one, and
152
+ for text-like fields an empty string as well.
153
+ - **Geo operators** work on `point` fields. Distances are great-circle
154
+ metres; polygon tests are planar.
155
+
156
+ ### Join fields
157
+
158
+ `join` fields are resolved for `find`, `findByID` and draft queries: one query
159
+ per target collection for all returned documents, grouped in memory, with
160
+ `limit`, `page`, `sort`, `where` and `count` applied per parent document.
161
+ Polymorphic joins (`collection: [...]`) return `{ relationTo, value }` entries.
162
+ Joined documents are read from the target's published rows.
163
+
164
+ ### Write guarantees
165
+
166
+ - **`create` is insert-only.** An id that is already taken is rejected with a
167
+ `ValidationError` on `id` rather than replacing the existing document.
168
+ - **No lost updates.** Updates are read-merge-write, conditional on the row's
169
+ revision. If another writer gets in first, the patch is re-applied to the
170
+ fresh row, so concurrent updates to different fields both land. This holds
171
+ per adapter call: Payload's own `update` operation fills the fields missing
172
+ from a patch with the values it read beforehand, so two concurrent
173
+ `payload.update` calls on one document can still overwrite each other.
174
+ - **Jobs are claimed once.** `updateJobs` uses the same check, so two workers
175
+ polling the queue can't both take the same job.
176
+ - **Bulk writes are bounded.** `updateMany`, `deleteMany` and `deleteVersions`
177
+ keep at most 25 requests in flight.
178
+ - **Oversized documents fail clearly.** DynamoDB caps an item at 400 KB;
179
+ exceeding it raises a 413 naming the document instead of a bare
180
+ `ValidationException`.
101
181
 
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).
182
+ ### Unique fields
183
+
184
+ `unique: true` is enforced for scalar fields at the top level or inside groups
185
+ and named tabs, including localized fields (per locale) and an auth
186
+ collection's `email` / `username`. Each value is claimed by a guard row
187
+ written in the same transaction as the document, so a duplicate is rejected
188
+ with a `ValidationError` on the field — also under concurrent writes. Blank
189
+ values (`null`, `''`) are not guarded. Unique fields inside arrays or blocks,
190
+ and `hasMany` fields, are not enforced.
191
+
192
+ Writes to a collection with unique fields are transactional and cost twice the
193
+ write units; collections without them are unaffected.
194
+
195
+ Documents written before this was enforced have no guards until they are next
196
+ saved. To cover them in one pass:
197
+
198
+ ```ts
199
+ import { backfillUniqueGuards } from '@aih-software/payload-ddb'
200
+
201
+ const report = await backfillUniqueGuards(payload)
202
+ // report.collections[slug].conflicts lists pre-existing duplicates
203
+ ```
108
204
 
109
205
  ## Known limits / future work
110
206
 
@@ -118,12 +214,22 @@ down).
118
214
  commit/rollback do nothing. Per-method atomicity (e.g. `createVersion`'s
119
215
  flip+put) uses DynamoDB `TransactWriteItems` already; full Payload-context
120
216
  transaction buffering across multiple adapter calls is its own milestone.
217
+ - **Filters cost a partition read.** Clauses evaluated in JS, relationship
218
+ paths and joins all read the partitions involved in full (a relationship
219
+ path or join adds one read of the related collection). Fine at CMS scale;
220
+ the GSI milestone is what removes it.
221
+ - **Joins don't see unpublished drafts** of the joined collection.
222
+ - **`locale: 'all'`** in a filter or sort falls back to the default locale.
223
+ - **`like` matches its operand as one substring.** Payload's Mongo adapter
224
+ splits the operand into words and requires each of them to match.
225
+ - **Items are limited to 400 KB**, including a version row's full snapshot.
121
226
 
122
227
  ## Development
123
228
 
124
229
  The package ships with a vitest integration test harness that boots Payload
125
230
  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.
231
+ workers can run in parallel against a single shared container, which listens
232
+ on port 8001. Use Node 24, the version CI runs.
127
233
 
128
234
  ```bash
129
235
  pnpm install
@@ -141,6 +247,23 @@ reachable. Override the host/port with `PAYLOAD_DDB_TEST_HOST` /
141
247
  See [`test/README.md`](./test/README.md) for the harness layout and how to
142
248
  add a new spec.
143
249
 
250
+ ### Releasing
251
+
252
+ Releases are cut by CI on every push to `main` that carries a
253
+ [changeset](https://github.com/changesets/changesets) (`pnpm changeset`):
254
+
255
+ 1. The integration tests run.
256
+ 2. The version is bumped, committed as `chore(release): vX.Y.Z` and tagged.
257
+ 3. The package is published to GitHub Packages as
258
+ `@aih-software/payload-ddb`, and a GitHub Release is created.
259
+ 4. For minor and major bumps, the same build is published to the public npm
260
+ registry as `@open-aih/payload-ddb`.
261
+
262
+ The public publish uses npm
263
+ [trusted publishing](https://docs.npmjs.com/trusted-publishers), so there is
264
+ no npm token to rotate. The trusted publisher configured on npmjs.com must
265
+ name this repository and the workflow file `release.yml`.
266
+
144
267
  ## License
145
268
 
146
269
  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,33 @@ 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. `unique` adds a guard row per
30
+ // unique field in the same transaction.
31
+ await putItem(this, partition, projected, {
32
+ mustNotExist: true,
33
+ unique: {
34
+ collection
35
+ }
36
+ });
37
+ break;
38
+ } catch (err) {
39
+ rethrowUniqueViolation(err, collection, req);
40
+ if (!isConditionalCheckFailed(err)) throw err;
41
+ // Either the id is taken, or this write collided with a concurrent one
42
+ // over a unique value. Surface the first the way the Mongo and Postgres
43
+ // adapters surface a duplicate primary key — a `ValidationError` on
44
+ // `id` — and retry the second, which resolves to a clean success or a
45
+ // unique violation.
46
+ if (await getById(this, partition, id)) throw mustBeUniqueError('id', collection, req);
47
+ if (attempt >= MAX_WRITE_ATTEMPTS) throw conflictError();
48
+ await backoff(attempt);
49
+ }
50
+ }
35
51
  return returning === false ? null : projected;
36
52
  };
37
53
 
@@ -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. `unique` adds a guard row per\n // unique field in the same transaction.\n await putItem(this, partition, projected, { mustNotExist: true, unique: { collection } })\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","unique","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,wEAAwE;YACxE,wCAAwC;YACxC,MAAMjB,QAAQ,IAAI,EAAEe,WAAWD,WAAW;gBAAEI,cAAc;gBAAMC,QAAQ;oBAAEjB;gBAAW;YAAE;YACvF;QACF,EAAE,OAAOkB,KAAK;YACZtB,uBAAuBsB,KAAKlB,YAAYG;YACxC,IAAI,CAACN,yBAAyBqB,MAAM,MAAMA;YAE1C,uEAAuE;YACvE,wEAAwE;YACxE,oEAAoE;YACpE,sEAAsE;YACtE,oBAAoB;YACpB,IAAI,MAAM1B,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;AAUlD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,mBAwCjC,CAAA"}
@@ -1,17 +1,12 @@
1
- import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
2
1
  import { randomUUID } from 'node:crypto';
3
2
  import { findFirst } from './utilities/findFirst.js';
4
- import { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
5
3
  import { projectVersionSnapshot } from './utilities/resolveSchema.js';
4
+ import { putVersionAsLatest } from './utilities/writeItems.js';
6
5
  /**
7
6
  * Like `createVersion` but for global singletons: there's no `parent`, and
8
7
  * the "latest" scope is the entire global's versions partition. Same atomic
9
- * flip+put via `TransactWriteItems`. See `createVersion` for rationale.
8
+ * flip+put via `putVersionAsLatest`. See `createVersion` for rationale.
10
9
  */ export const createGlobalVersion = async function createGlobalVersion({ autosave, createdAt, globalSlug, publishedLocale, returning, snapshot, updatedAt, versionData }) {
11
- const docClient = this.docClient;
12
- if (!docClient) {
13
- throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
14
- }
15
10
  const partition = this.resolveVersionsPartition(globalSlug);
16
11
  const previousLatest = await findFirst(this, {
17
12
  partition,
@@ -40,40 +35,7 @@ import { projectVersionSnapshot } from './utilities/resolveSchema.js';
40
35
  publishedLocale
41
36
  } : {}
42
37
  };
43
- const putItem = normalizeForDynamo({
44
- ...item,
45
- pk: partition,
46
- sk: id
47
- });
48
- const transactItems = [];
49
- if (previousLatest) {
50
- transactItems.push({
51
- Update: {
52
- TableName: this.tableName,
53
- Key: {
54
- pk: partition,
55
- sk: String(previousLatest['id'])
56
- },
57
- UpdateExpression: 'SET #latest = :false',
58
- ExpressionAttributeNames: {
59
- '#latest': 'latest'
60
- },
61
- ExpressionAttributeValues: {
62
- ':false': false
63
- },
64
- ConditionExpression: 'attribute_exists(pk)'
65
- }
66
- });
67
- }
68
- transactItems.push({
69
- Put: {
70
- TableName: this.tableName,
71
- Item: putItem
72
- }
73
- });
74
- await docClient.send(new TransactWriteCommand({
75
- TransactItems: transactItems
76
- }));
38
+ await putVersionAsLatest(this, partition, item, previousLatest);
77
39
  return returning === false ? null : item;
78
40
  };
79
41
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/createGlobalVersion.ts"],"sourcesContent":["import type { CreateGlobalVersion } from 'payload'\n\nimport { TransactWriteCommand } from '@aws-sdk/lib-dynamodb'\nimport { randomUUID } from 'node:crypto'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { findFirst } from './utilities/findFirst.js'\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { projectVersionSnapshot } from './utilities/resolveSchema.js'\n\n/**\n * Like `createVersion` but for global singletons: there's no `parent`, and\n * the \"latest\" scope is the entire global's versions partition. Same atomic\n * flip+put via `TransactWriteItems`. See `createVersion` for rationale.\n */\nexport const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(\n this: DynamoAdapter,\n {\n autosave,\n createdAt,\n globalSlug,\n publishedLocale,\n returning,\n snapshot,\n updatedAt,\n versionData,\n },\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 partition = this.resolveVersionsPartition(globalSlug)\n\n const previousLatest = await findFirst(this, {\n partition,\n where: { latest: { equals: true } },\n })\n\n const id = randomUUID()\n const sanitizedVersionData = projectVersionSnapshot(\n this,\n { kind: 'global', slug: globalSlug },\n versionData as Record<string, unknown>,\n )\n const item: Record<string, unknown> = {\n id,\n version: sanitizedVersionData,\n createdAt,\n updatedAt,\n latest: true,\n autosave,\n ...(snapshot ? { snapshot: true } : {}),\n ...(publishedLocale !== undefined ? { publishedLocale } : {}),\n }\n\n const putItem = normalizeForDynamo({\n ...item,\n pk: partition,\n sk: id,\n })\n\n const transactItems: NonNullable<\n ConstructorParameters<typeof TransactWriteCommand>[0]['TransactItems']\n > = []\n\n if (previousLatest) {\n transactItems.push({\n Update: {\n TableName: this.tableName,\n Key: { pk: partition, sk: String(previousLatest['id']) },\n UpdateExpression: 'SET #latest = :false',\n ExpressionAttributeNames: { '#latest': 'latest' },\n ExpressionAttributeValues: { ':false': false },\n ConditionExpression: 'attribute_exists(pk)',\n },\n })\n }\n\n transactItems.push({\n Put: {\n TableName: this.tableName,\n Item: putItem,\n },\n })\n\n await docClient.send(new TransactWriteCommand({ TransactItems: transactItems }))\n\n return returning === false ? (null as never) : (item as never)\n}\n"],"names":["TransactWriteCommand","randomUUID","findFirst","normalizeForDynamo","projectVersionSnapshot","createGlobalVersion","autosave","createdAt","globalSlug","publishedLocale","returning","snapshot","updatedAt","versionData","docClient","Error","partition","resolveVersionsPartition","previousLatest","where","latest","equals","id","sanitizedVersionData","kind","slug","item","version","undefined","putItem","pk","sk","transactItems","push","Update","TableName","tableName","Key","String","UpdateExpression","ExpressionAttributeNames","ExpressionAttributeValues","ConditionExpression","Put","Item","send","TransactItems"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,wBAAuB;AAC5D,SAASC,UAAU,QAAQ,cAAa;AAIxC,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,sBAAsB,QAAQ,+BAA8B;AAErE;;;;CAIC,GACD,OAAO,MAAMC,sBAA2C,eAAeA,oBAErE,EACEC,QAAQ,EACRC,SAAS,EACTC,UAAU,EACVC,eAAe,EACfC,SAAS,EACTC,QAAQ,EACRC,SAAS,EACTC,WAAW,EACZ;IAED,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,YAAY,IAAI,CAACC,wBAAwB,CAACT;IAEhD,MAAMU,iBAAiB,MAAMhB,UAAU,IAAI,EAAE;QAC3Cc;QACAG,OAAO;YAAEC,QAAQ;gBAAEC,QAAQ;YAAK;QAAE;IACpC;IAEA,MAAMC,KAAKrB;IACX,MAAMsB,uBAAuBnB,uBAC3B,IAAI,EACJ;QAAEoB,MAAM;QAAUC,MAAMjB;IAAW,GACnCK;IAEF,MAAMa,OAAgC;QACpCJ;QACAK,SAASJ;QACThB;QACAK;QACAQ,QAAQ;QACRd;QACA,GAAIK,WAAW;YAAEA,UAAU;QAAK,IAAI,CAAC,CAAC;QACtC,GAAIF,oBAAoBmB,YAAY;YAAEnB;QAAgB,IAAI,CAAC,CAAC;IAC9D;IAEA,MAAMoB,UAAU1B,mBAAmB;QACjC,GAAGuB,IAAI;QACPI,IAAId;QACJe,IAAIT;IACN;IAEA,MAAMU,gBAEF,EAAE;IAEN,IAAId,gBAAgB;QAClBc,cAAcC,IAAI,CAAC;YACjBC,QAAQ;gBACNC,WAAW,IAAI,CAACC,SAAS;gBACzBC,KAAK;oBAAEP,IAAId;oBAAWe,IAAIO,OAAOpB,cAAc,CAAC,KAAK;gBAAE;gBACvDqB,kBAAkB;gBAClBC,0BAA0B;oBAAE,WAAW;gBAAS;gBAChDC,2BAA2B;oBAAE,UAAU;gBAAM;gBAC7CC,qBAAqB;YACvB;QACF;IACF;IAEAV,cAAcC,IAAI,CAAC;QACjBU,KAAK;YACHR,WAAW,IAAI,CAACC,SAAS;YACzBQ,MAAMf;QACR;IACF;IAEA,MAAMf,UAAU+B,IAAI,CAAC,IAAI7C,qBAAqB;QAAE8C,eAAed;IAAc;IAE7E,OAAOtB,cAAc,QAAS,OAAkBgB;AAClD,EAAC"}
1
+ {"version":3,"sources":["../src/createGlobalVersion.ts"],"sourcesContent":["import type { CreateGlobalVersion } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { findFirst } from './utilities/findFirst.js'\nimport { projectVersionSnapshot } from './utilities/resolveSchema.js'\nimport { putVersionAsLatest } from './utilities/writeItems.js'\n\n/**\n * Like `createVersion` but for global singletons: there's no `parent`, and\n * the \"latest\" scope is the entire global's versions partition. Same atomic\n * flip+put via `putVersionAsLatest`. See `createVersion` for rationale.\n */\nexport const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(\n this: DynamoAdapter,\n {\n autosave,\n createdAt,\n globalSlug,\n publishedLocale,\n returning,\n snapshot,\n updatedAt,\n versionData,\n },\n) {\n const partition = this.resolveVersionsPartition(globalSlug)\n\n const previousLatest = await findFirst(this, {\n partition,\n where: { latest: { equals: true } },\n })\n\n const id = randomUUID()\n const sanitizedVersionData = projectVersionSnapshot(\n this,\n { kind: 'global', slug: globalSlug },\n versionData as Record<string, unknown>,\n )\n const item: Record<string, unknown> = {\n id,\n version: sanitizedVersionData,\n createdAt,\n updatedAt,\n latest: true,\n autosave,\n ...(snapshot ? { snapshot: true } : {}),\n ...(publishedLocale !== undefined ? { publishedLocale } : {}),\n }\n\n await putVersionAsLatest(this, partition, item, previousLatest)\n\n return returning === false ? (null as never) : (item as never)\n}\n"],"names":["randomUUID","findFirst","projectVersionSnapshot","putVersionAsLatest","createGlobalVersion","autosave","createdAt","globalSlug","publishedLocale","returning","snapshot","updatedAt","versionData","partition","resolveVersionsPartition","previousLatest","where","latest","equals","id","sanitizedVersionData","kind","slug","item","version","undefined"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AAIxC,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,sBAAsB,QAAQ,+BAA8B;AACrE,SAASC,kBAAkB,QAAQ,4BAA2B;AAE9D;;;;CAIC,GACD,OAAO,MAAMC,sBAA2C,eAAeA,oBAErE,EACEC,QAAQ,EACRC,SAAS,EACTC,UAAU,EACVC,eAAe,EACfC,SAAS,EACTC,QAAQ,EACRC,SAAS,EACTC,WAAW,EACZ;IAED,MAAMC,YAAY,IAAI,CAACC,wBAAwB,CAACP;IAEhD,MAAMQ,iBAAiB,MAAMd,UAAU,IAAI,EAAE;QAC3CY;QACAG,OAAO;YAAEC,QAAQ;gBAAEC,QAAQ;YAAK;QAAE;IACpC;IAEA,MAAMC,KAAKnB;IACX,MAAMoB,uBAAuBlB,uBAC3B,IAAI,EACJ;QAAEmB,MAAM;QAAUC,MAAMf;IAAW,GACnCK;IAEF,MAAMW,OAAgC;QACpCJ;QACAK,SAASJ;QACTd;QACAK;QACAM,QAAQ;QACRZ;QACA,GAAIK,WAAW;YAAEA,UAAU;QAAK,IAAI,CAAC,CAAC;QACtC,GAAIF,oBAAoBiB,YAAY;YAAEjB;QAAgB,IAAI,CAAC,CAAC;IAC9D;IAEA,MAAML,mBAAmB,IAAI,EAAEU,WAAWU,MAAMR;IAEhD,OAAON,cAAc,QAAS,OAAkBc;AAClD,EAAC"}
@@ -5,11 +5,12 @@ import type { CreateVersion } from 'payload';
5
5
  *
6
6
  * Two round trips:
7
7
  * 1. `Query` the versions partition for the previous `latest=true` row.
8
- * 2. `TransactWriteItems` to flip that row's `latest` to false and put the
9
- * new row in one atomic call. If a crash occurs mid-flow neither
10
- * mutation lands, so we never end up with two `latest=true` rows or
11
- * zero. The Update carries `attribute_exists(pk)` so the transaction
12
- * fails cleanly if the previous row was deleted between (1) and (2).
8
+ * 2. `putVersionAsLatest` flips that row's `latest` to false and puts the
9
+ * new row in one atomic `TransactWriteItems` call. If a crash occurs
10
+ * mid-flow neither mutation lands, so we never end up with two
11
+ * `latest=true` rows or zero. The flip carries `attribute_exists(pk)` so
12
+ * the transaction fails cleanly if the previous row was deleted between
13
+ * (1) and (2).
13
14
  *
14
15
  * `autosave` is persisted on the row even though it isn't surfaced in
15
16
  * `TypeWithVersion` — `findVersions` filters by it.
@@ -1 +1 @@
1
- {"version":3,"file":"createVersion.d.ts","sourceRoot":"","sources":["../src/createVersion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAW5C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,EAAE,aAmF3B,CAAA"}
1
+ {"version":3,"file":"createVersion.d.ts","sourceRoot":"","sources":["../src/createVersion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAU5C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,aA6C3B,CAAA"}