@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
@@ -0,0 +1,254 @@
1
+ import { createArrayFromCommaDelineated } from 'payload';
2
+ import { fieldShouldBeLocalized } from 'payload/shared';
3
+ import { JS_ONLY_MARKER } from './operators.js';
4
+ /** Field types whose stored value can be an empty string. */ const CAN_HOLD_EMPTY_STRING = new Set([
5
+ 'code',
6
+ 'email',
7
+ 'radio',
8
+ 'select',
9
+ 'text',
10
+ 'textarea'
11
+ ]);
12
+ /**
13
+ * Operators whose operand is a search needle, a flag or a geometry — not a
14
+ * value of the field's own type.
15
+ */ const UNCAST_OPERATORS = new Set([
16
+ 'intersects',
17
+ 'like',
18
+ 'near',
19
+ 'not_like',
20
+ 'within'
21
+ ]);
22
+ const LIST_OPERATORS = new Set([
23
+ 'all',
24
+ 'in',
25
+ 'not_in'
26
+ ]);
27
+ export function castWhere(where, ctx) {
28
+ if (!where || typeof where !== 'object') return where;
29
+ const out = {};
30
+ // Clauses a rewrite needs AND-ed onto this level (see `exists` below).
31
+ const extra = [];
32
+ for (const [key, raw] of Object.entries(where)){
33
+ if (key === 'and' || key === 'or') {
34
+ out[key] = Array.isArray(raw) ? raw.map((sub)=>sub && typeof sub === 'object' ? castWhere(sub, ctx) : sub) : raw;
35
+ continue;
36
+ }
37
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
38
+ out[key] = raw;
39
+ continue;
40
+ }
41
+ const { field, multi, path } = resolvePath(ctx.fields, key, ctx.localization);
42
+ const cast = casterFor(key, field, ctx);
43
+ /** A clause on this path, carrying the JS-only marker when it needs one. */ const clause = (operators)=>({
44
+ [path]: multi ? {
45
+ ...operators,
46
+ [JS_ONLY_MARKER]: true
47
+ } : operators
48
+ });
49
+ const castList = (operand)=>Array.isArray(operand) ? operand.map(cast) : cast(operand);
50
+ const operators = {};
51
+ for (const [operator, operand] of Object.entries(raw)){
52
+ if (operator === 'exists') {
53
+ const wanted = toBoolean(operand);
54
+ if (!field || !holdsEmptyString(field)) {
55
+ operators['exists'] = wanted;
56
+ } else if (wanted) {
57
+ // Present = has a value, and that value isn't the empty string.
58
+ operators['exists'] = true;
59
+ extra.push(clause({
60
+ not_equals: ''
61
+ }));
62
+ } else {
63
+ // Absent = no value, or an empty string.
64
+ extra.push({
65
+ or: [
66
+ clause({
67
+ exists: false
68
+ }),
69
+ clause({
70
+ equals: ''
71
+ })
72
+ ]
73
+ });
74
+ }
75
+ continue;
76
+ }
77
+ if (operator === 'contains') {
78
+ if (field && holdsDiscreteValues(field)) {
79
+ // "Contains" an option or an id means "has it" — there's no
80
+ // substring to search.
81
+ const value = castList(operand);
82
+ extra.push(clause(Array.isArray(value) ? {
83
+ in: value
84
+ } : {
85
+ equals: value
86
+ }));
87
+ } else {
88
+ operators['contains'] = operand;
89
+ }
90
+ continue;
91
+ }
92
+ if (UNCAST_OPERATORS.has(operator)) {
93
+ operators[operator] = operand;
94
+ continue;
95
+ }
96
+ operators[operator] = castList(LIST_OPERATORS.has(operator) && typeof operand === 'string' ? createArrayFromCommaDelineated(operand) : operand);
97
+ }
98
+ if (Object.keys(operators).length > 0) {
99
+ Object.assign(out, clause(operators));
100
+ }
101
+ }
102
+ if (extra.length > 0) {
103
+ // Sibling keys of a `Where` are AND-ed together, as are the members of
104
+ // its `and` — so appending there keeps the meaning and avoids colliding
105
+ // with an operator the caller already put on the same path.
106
+ out.and = [
107
+ ...Array.isArray(out.and) ? out.and : [],
108
+ ...extra
109
+ ];
110
+ }
111
+ return out;
112
+ }
113
+ /**
114
+ * Walk a dotted path through groups, named tabs, arrays and blocks to the
115
+ * field it names, noting on the way whether it crosses anything list-shaped
116
+ * and where locale keys belong.
117
+ *
118
+ * `field` is `null` when the path leaves the declared schema — an unknown
119
+ * name, or a path that continues past a field with no sub-fields (a
120
+ * polymorphic relationship's `.value`, a key inside `json`), where guessing
121
+ * a type would be wrong.
122
+ */ export function resolvePath(fields, path, localization) {
123
+ const segments = path.split('.');
124
+ const stored = [];
125
+ let current = fields;
126
+ let field = null;
127
+ let multi = false;
128
+ let parentIsLocalized = false;
129
+ for(let index = 0; index < segments.length; index++){
130
+ const segment = segments[index];
131
+ stored.push(segment);
132
+ const found = current?.find((candidate)=>candidate.name === segment);
133
+ if (!found) {
134
+ // Off the schema from here on; copy the rest of the path as given.
135
+ current = null;
136
+ field = null;
137
+ continue;
138
+ }
139
+ field = found;
140
+ if (localization && fieldShouldBeLocalized({
141
+ field: found,
142
+ parentIsLocalized
143
+ })) {
144
+ const next = segments[index + 1];
145
+ if (next !== undefined && localization.localeCodes.includes(next)) {
146
+ // The caller named a locale explicitly — keep theirs.
147
+ stored.push(next);
148
+ index++;
149
+ } else {
150
+ stored.push(localization.locale);
151
+ }
152
+ parentIsLocalized = true;
153
+ }
154
+ if (found.type === 'array' || found.type === 'blocks' || 'hasMany' in found && found.hasMany) {
155
+ multi = true;
156
+ }
157
+ if (index === segments.length - 1) break;
158
+ if ('flattenedFields' in found) {
159
+ current = found.flattenedFields;
160
+ } else if (found.type === 'blocks') {
161
+ // Block rows are stored side by side in one list, so a sub-field name
162
+ // is looked up across every block type.
163
+ current = found.blocks.flatMap((block)=>block.flattenedFields);
164
+ } else {
165
+ current = null;
166
+ // Relationships keep a known shape below them (`.value`, `.relationTo`).
167
+ // Anything else — `json`, rich text — may hold lists we can't see.
168
+ if (found.type !== 'relationship' && found.type !== 'upload') multi = true;
169
+ }
170
+ }
171
+ return {
172
+ field,
173
+ multi,
174
+ path: stored.join('.')
175
+ };
176
+ }
177
+ /** `hasMany` selects, and relationships of either arity, hold whole values to match — not text to search. */ function holdsDiscreteValues(field) {
178
+ if (field.type === 'relationship' || field.type === 'upload') return true;
179
+ return field.type === 'select' && Boolean(field.hasMany);
180
+ }
181
+ function holdsEmptyString(field) {
182
+ if (!CAN_HOLD_EMPTY_STRING.has(field.type)) return false;
183
+ // `hasMany` text/select fields store a list, never a bare string.
184
+ return !('hasMany' in field && field.hasMany);
185
+ }
186
+ function casterFor(path, field, ctx) {
187
+ if (!field) {
188
+ // `id` is framework-owned and only shows up in `fields` when a
189
+ // collection declares a custom one.
190
+ return path === 'id' ? (value)=>castId(value, ctx.idType) : keep;
191
+ }
192
+ switch(field.type){
193
+ case 'checkbox':
194
+ return castCheckbox;
195
+ case 'date':
196
+ return castDate;
197
+ case 'number':
198
+ return castNumber;
199
+ case 'relationship':
200
+ case 'upload':
201
+ {
202
+ const { relationTo } = field;
203
+ return (value)=>{
204
+ if (value === 'null') return null;
205
+ if (typeof relationTo === 'string') return castId(value, ctx.idTypeOf(relationTo));
206
+ // Polymorphic: only the `{ relationTo, value }` form says which
207
+ // collection the id belongs to.
208
+ if (isRelationValue(value)) {
209
+ return {
210
+ ...value,
211
+ value: castId(value.value, ctx.idTypeOf(value.relationTo))
212
+ };
213
+ }
214
+ return value;
215
+ };
216
+ }
217
+ default:
218
+ return keep;
219
+ }
220
+ }
221
+ const keep = (value)=>value;
222
+ function toBoolean(value) {
223
+ return value === true || typeof value === 'string' && value.toLowerCase() === 'true';
224
+ }
225
+ function castCheckbox(value) {
226
+ if (typeof value !== 'string') return value;
227
+ const lowered = value.toLowerCase();
228
+ if (lowered === 'true') return true;
229
+ if (lowered === 'false') return false;
230
+ return value;
231
+ }
232
+ function castNumber(value) {
233
+ if (typeof value !== 'string' || value.trim() === '') return value;
234
+ const parsed = Number(value);
235
+ return Number.isFinite(parsed) ? parsed : value;
236
+ }
237
+ function castDate(value) {
238
+ const isCandidate = typeof value === 'string' && value.trim() !== '' || typeof value === 'number' && Number.isFinite(value) || value instanceof Date;
239
+ if (!isCandidate) return value;
240
+ const parsed = new Date(value);
241
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
242
+ }
243
+ /**
244
+ * Text ids are left alone on purpose — a text-id collection can still hold a
245
+ * numeric `id` (a number passed as `customID`), so stringifying operands
246
+ * would break lookups that work today.
247
+ */ function castId(value, idType) {
248
+ return idType === 'number' ? castNumber(value) : value;
249
+ }
250
+ function isRelationValue(value) {
251
+ return typeof value === 'object' && value !== null && typeof value.relationTo === 'string' && 'value' in value;
252
+ }
253
+
254
+ //# sourceMappingURL=castWhere.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/castWhere.ts"],"sourcesContent":["import type { FlattenedField, Where } from 'payload'\n\nimport { createArrayFromCommaDelineated } from 'payload'\nimport { fieldShouldBeLocalized } from 'payload/shared'\n\nimport { JS_ONLY_MARKER } from './operators.js'\n\n/**\n * Rewrite a Payload `Where` into the terms the stored rows are in, before\n * either evaluator (`buildFilterExpression` or `matchesWhere`) sees it. Three\n * things happen per clause, all driven by the field the path points at:\n *\n * **Operands are cast** to the type the field stores. They routinely arrive\n * as strings: the REST API parses `?where[price][greater_than]=10` with\n * `qs`, which yields `\"10\"`, and the admin list filters go the same way.\n * Mongoose and SQL drivers quietly coerce those against their schema.\n * DynamoDB has no schema and compares strictly by type — `\"10\"` never equals\n * or orders against the number `10` — so without this such a filter matches\n * nothing and reports no error. The rules follow Payload's Mongo and\n * Postgres adapters (`sanitizeQueryValue`), minus their database quirks:\n *\n * - `checkbox`: `'true'` / `'false'` (any case) → boolean.\n * - `number`: numeric strings → number.\n * - `date`: parseable strings, epoch-ms numbers and `Date`s → the full ISO\n * string we store. `'2026-01-01'` must become `'2026-01-01T00:00:00.000Z'`\n * or it would compare as a mere prefix of the stored value.\n * - `relationship` / `upload`: `'null'` → `null`; ids → number when the\n * related collection has numeric ids, including inside a polymorphic\n * `{ relationTo, value }` operand.\n * - `id`: → number when this collection has numeric ids.\n * - `in` / `not_in` / `all`: a comma-delimited string → array, each member\n * cast.\n * - `exists`: operand → boolean (`'false'` is a truthy string otherwise),\n * and for fields that can hold text, an empty string counts as absent —\n * that's what the admin UI leaves behind when a text input is cleared.\n * - `contains` on a `hasMany` select or a relationship means \"has this\n * value\", so it becomes `equals` / `in`.\n *\n * **Localized paths get their locale key.** A localized field is stored as\n * `{ en: …, es: … }`, so `title` becomes `title.en` for the request's locale\n * (unless the caller already wrote a locale into the path).\n *\n * **Paths DynamoDB can't address are marked** with `JS_ONLY_MARKER`: any that\n * run through an `array` / `blocks` field or a `hasMany` value, or into a\n * field whose inner shape the schema doesn't describe (`json`, rich text).\n * The pushdown skips those clauses; `matchesWhere` evaluates them with\n * any-element semantics.\n *\n * Anything it can't confidently cast is left exactly as given: an unknown\n * path, a path that continues past a leaf field, an unparseable value.\n *\n * Returns a new `Where`; the caller's object is never mutated.\n */\n\nexport type IdType = 'number' | 'text'\n\nexport type Localization = {\n /** Locale to address when a path doesn't name one. */\n locale: string\n localeCodes: string[]\n}\n\nexport type CastContext = {\n /** Flattened fields of the entity the `where` targets. */\n fields: FlattenedField[]\n /** Type of the entity's own `id` when it isn't declared as a field. */\n idType: IdType\n /** Id type of another collection, for relationship operands. */\n idTypeOf: (collectionSlug: string) => IdType\n /** Set only when the Payload config has localization enabled. */\n localization?: Localization\n}\n\n/** Field types whose stored value can be an empty string. */\nconst CAN_HOLD_EMPTY_STRING = new Set(['code', 'email', 'radio', 'select', 'text', 'textarea'])\n\n/**\n * Operators whose operand is a search needle, a flag or a geometry — not a\n * value of the field's own type.\n */\nconst UNCAST_OPERATORS = new Set(['intersects', 'like', 'near', 'not_like', 'within'])\n\nconst LIST_OPERATORS = new Set(['all', 'in', 'not_in'])\n\nexport function castWhere(where: undefined | Where, ctx: CastContext): undefined | Where {\n if (!where || typeof where !== 'object') return where\n\n const out: Where = {}\n // Clauses a rewrite needs AND-ed onto this level (see `exists` below).\n const extra: Where[] = []\n\n for (const [key, raw] of Object.entries(where)) {\n if (key === 'and' || key === 'or') {\n out[key] = Array.isArray(raw)\n ? raw.map((sub) => (sub && typeof sub === 'object' ? castWhere(sub, ctx)! : sub))\n : (raw as Where[])\n continue\n }\n\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n out[key] = raw as Where[string]\n continue\n }\n\n const { field, multi, path } = resolvePath(ctx.fields, key, ctx.localization)\n const cast = casterFor(key, field, ctx)\n /** A clause on this path, carrying the JS-only marker when it needs one. */\n const clause = (operators: Record<string, unknown>): Where => ({\n [path]: (multi ? { ...operators, [JS_ONLY_MARKER]: true } : operators) as Where[string],\n })\n const castList = (operand: unknown): unknown =>\n Array.isArray(operand) ? operand.map(cast) : cast(operand)\n\n const operators: Record<string, unknown> = {}\n\n for (const [operator, operand] of Object.entries(raw as Record<string, unknown>)) {\n if (operator === 'exists') {\n const wanted = toBoolean(operand)\n if (!field || !holdsEmptyString(field)) {\n operators['exists'] = wanted\n } else if (wanted) {\n // Present = has a value, and that value isn't the empty string.\n operators['exists'] = true\n extra.push(clause({ not_equals: '' }))\n } else {\n // Absent = no value, or an empty string.\n extra.push({ or: [clause({ exists: false }), clause({ equals: '' })] })\n }\n continue\n }\n\n if (operator === 'contains') {\n if (field && holdsDiscreteValues(field)) {\n // \"Contains\" an option or an id means \"has it\" — there's no\n // substring to search.\n const value = castList(operand)\n extra.push(clause(Array.isArray(value) ? { in: value } : { equals: value }))\n } else {\n operators['contains'] = operand\n }\n continue\n }\n\n if (UNCAST_OPERATORS.has(operator)) {\n operators[operator] = operand\n continue\n }\n\n operators[operator] = castList(\n LIST_OPERATORS.has(operator) && typeof operand === 'string'\n ? createArrayFromCommaDelineated(operand)\n : operand,\n )\n }\n\n if (Object.keys(operators).length > 0) {\n Object.assign(out, clause(operators))\n }\n }\n\n if (extra.length > 0) {\n // Sibling keys of a `Where` are AND-ed together, as are the members of\n // its `and` — so appending there keeps the meaning and avoids colliding\n // with an operator the caller already put on the same path.\n out.and = [...(Array.isArray(out.and) ? out.and : []), ...extra]\n }\n\n return out\n}\n\nexport type ResolvedPath = {\n /** The field the path ends on, when the schema describes it. */\n field: FlattenedField | null\n /** True when DynamoDB can't address the path as one value. */\n multi: boolean\n /** The path as stored: locale keys inserted after localized segments. */\n path: string\n}\n\n/**\n * Walk a dotted path through groups, named tabs, arrays and blocks to the\n * field it names, noting on the way whether it crosses anything list-shaped\n * and where locale keys belong.\n *\n * `field` is `null` when the path leaves the declared schema — an unknown\n * name, or a path that continues past a field with no sub-fields (a\n * polymorphic relationship's `.value`, a key inside `json`), where guessing\n * a type would be wrong.\n */\nexport function resolvePath(\n fields: FlattenedField[],\n path: string,\n localization?: Localization,\n): ResolvedPath {\n const segments = path.split('.')\n const stored: string[] = []\n let current: FlattenedField[] | null = fields\n let field: FlattenedField | null = null\n let multi = false\n let parentIsLocalized = false\n\n for (let index = 0; index < segments.length; index++) {\n const segment = segments[index]!\n stored.push(segment)\n\n const found: FlattenedField | undefined = current?.find(\n (candidate) => candidate.name === segment,\n )\n if (!found) {\n // Off the schema from here on; copy the rest of the path as given.\n current = null\n field = null\n continue\n }\n field = found\n\n if (localization && fieldShouldBeLocalized({ field: found, parentIsLocalized })) {\n const next = segments[index + 1]\n if (next !== undefined && localization.localeCodes.includes(next)) {\n // The caller named a locale explicitly — keep theirs.\n stored.push(next)\n index++\n } else {\n stored.push(localization.locale)\n }\n parentIsLocalized = true\n }\n\n if (found.type === 'array' || found.type === 'blocks' || ('hasMany' in found && found.hasMany)) {\n multi = true\n }\n\n if (index === segments.length - 1) break\n\n if ('flattenedFields' in found) {\n current = found.flattenedFields\n } else if (found.type === 'blocks') {\n // Block rows are stored side by side in one list, so a sub-field name\n // is looked up across every block type.\n current = found.blocks.flatMap((block) => block.flattenedFields)\n } else {\n current = null\n // Relationships keep a known shape below them (`.value`, `.relationTo`).\n // Anything else — `json`, rich text — may hold lists we can't see.\n if (found.type !== 'relationship' && found.type !== 'upload') multi = true\n }\n }\n\n return { field, multi, path: stored.join('.') }\n}\n\n/** `hasMany` selects, and relationships of either arity, hold whole values to match — not text to search. */\nfunction holdsDiscreteValues(field: FlattenedField): boolean {\n if (field.type === 'relationship' || field.type === 'upload') return true\n return field.type === 'select' && Boolean(field.hasMany)\n}\n\nfunction holdsEmptyString(field: FlattenedField): boolean {\n if (!CAN_HOLD_EMPTY_STRING.has(field.type)) return false\n // `hasMany` text/select fields store a list, never a bare string.\n return !('hasMany' in field && field.hasMany)\n}\n\nfunction casterFor(\n path: string,\n field: FlattenedField | null,\n ctx: CastContext,\n): (value: unknown) => unknown {\n if (!field) {\n // `id` is framework-owned and only shows up in `fields` when a\n // collection declares a custom one.\n return path === 'id' ? (value) => castId(value, ctx.idType) : keep\n }\n\n switch (field.type) {\n case 'checkbox':\n return castCheckbox\n case 'date':\n return castDate\n case 'number':\n return castNumber\n case 'relationship':\n case 'upload': {\n const { relationTo } = field\n return (value) => {\n if (value === 'null') return null\n if (typeof relationTo === 'string') return castId(value, ctx.idTypeOf(relationTo))\n // Polymorphic: only the `{ relationTo, value }` form says which\n // collection the id belongs to.\n if (isRelationValue(value)) {\n return { ...value, value: castId(value.value, ctx.idTypeOf(value.relationTo)) }\n }\n return value\n }\n }\n default:\n return keep\n }\n}\n\nconst keep = (value: unknown): unknown => value\n\nfunction toBoolean(value: unknown): boolean {\n return value === true || (typeof value === 'string' && value.toLowerCase() === 'true')\n}\n\nfunction castCheckbox(value: unknown): unknown {\n if (typeof value !== 'string') return value\n const lowered = value.toLowerCase()\n if (lowered === 'true') return true\n if (lowered === 'false') return false\n return value\n}\n\nfunction castNumber(value: unknown): unknown {\n if (typeof value !== 'string' || value.trim() === '') return value\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : value\n}\n\nfunction castDate(value: unknown): unknown {\n const isCandidate =\n (typeof value === 'string' && value.trim() !== '') ||\n (typeof value === 'number' && Number.isFinite(value)) ||\n value instanceof Date\n if (!isCandidate) return value\n const parsed = new Date(value as Date | number | string)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString()\n}\n\n/**\n * Text ids are left alone on purpose — a text-id collection can still hold a\n * numeric `id` (a number passed as `customID`), so stringifying operands\n * would break lookups that work today.\n */\nfunction castId(value: unknown, idType: IdType): unknown {\n return idType === 'number' ? castNumber(value) : value\n}\n\nfunction isRelationValue(value: unknown): value is { relationTo: string; value: unknown } {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { relationTo?: unknown }).relationTo === 'string' &&\n 'value' in value\n )\n}\n"],"names":["createArrayFromCommaDelineated","fieldShouldBeLocalized","JS_ONLY_MARKER","CAN_HOLD_EMPTY_STRING","Set","UNCAST_OPERATORS","LIST_OPERATORS","castWhere","where","ctx","out","extra","key","raw","Object","entries","Array","isArray","map","sub","field","multi","path","resolvePath","fields","localization","cast","casterFor","clause","operators","castList","operand","operator","wanted","toBoolean","holdsEmptyString","push","not_equals","or","exists","equals","holdsDiscreteValues","value","in","has","keys","length","assign","and","segments","split","stored","current","parentIsLocalized","index","segment","found","find","candidate","name","next","undefined","localeCodes","includes","locale","type","hasMany","flattenedFields","blocks","flatMap","block","join","Boolean","castId","idType","keep","castCheckbox","castDate","castNumber","relationTo","idTypeOf","isRelationValue","toLowerCase","lowered","trim","parsed","Number","isFinite","isCandidate","Date","isNaN","getTime","toISOString"],"mappings":"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"}
@@ -6,8 +6,9 @@ import type { DynamoAdapter } from '../types.js';
6
6
  *
7
7
  * Fast path: if `where` is a pure id-equality, do a single `GetItem` with the
8
8
  * full composite key.
9
- * Slow path: paginate `Query` (scoped by `pk = partition`) with
10
- * `FilterExpression` pushdown and stop on the first matching row in any page.
9
+ * Slow path: walk the partition with as much of `where` pushed down as
10
+ * DynamoDB can express, re-check rows with `matchesWhere` when that pushdown
11
+ * is inexact, and stop on the first match in any page.
11
12
  */
12
13
  export declare function findFirst(adapter: DynamoAdapter, args: {
13
14
  partition: string;
@@ -1 +1 @@
1
- {"version":3,"file":"findFirst.d.ts","sourceRoot":"","sources":["../../src/utilities/findFirst.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAShD;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,KAAK,CAAA;CAAE,GACpD,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA+DzC"}
1
+ {"version":3,"file":"findFirst.d.ts","sourceRoot":"","sources":["../../src/utilities/findFirst.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAQhD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,KAAK,CAAA;CAAE,GACpD,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAgCzC"}
@@ -1,8 +1,7 @@
1
- import { GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
1
+ import { GetCommand } from '@aws-sdk/lib-dynamodb';
2
2
  import { buildFilterExpression } from './buildFilterExpression.js';
3
3
  import { matchesWhere } from './matchesWhere.js';
4
- import { whereHasJsOnlyOperator } from './operators.js';
5
- import { queryMatching } from './queryMatching.js';
4
+ import { partitionPages } from './queryPartition.js';
6
5
  import { stripInternalKeys } from './stripInternalKeys.js';
7
6
  import { whereToId } from './whereToId.js';
8
7
  /**
@@ -11,15 +10,16 @@ import { whereToId } from './whereToId.js';
11
10
  *
12
11
  * Fast path: if `where` is a pure id-equality, do a single `GetItem` with the
13
12
  * full composite key.
14
- * Slow path: paginate `Query` (scoped by `pk = partition`) with
15
- * `FilterExpression` pushdown and stop on the first matching row in any page.
13
+ * Slow path: walk the partition with as much of `where` pushed down as
14
+ * DynamoDB can express, re-check rows with `matchesWhere` when that pushdown
15
+ * is inexact, and stop on the first match in any page.
16
16
  */ export async function findFirst(adapter, args) {
17
17
  const docClient = adapter.docClient;
18
18
  if (!docClient) {
19
19
  throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
20
20
  }
21
- const { partition } = args;
22
- const id = whereToId(args.where);
21
+ const { partition, where } = args;
22
+ const id = whereToId(where);
23
23
  if (id !== null) {
24
24
  const result = await docClient.send(new GetCommand({
25
25
  TableName: adapter.tableName,
@@ -32,48 +32,16 @@ import { whereToId } from './whereToId.js';
32
32
  }));
33
33
  return result.Item ? stripInternalKeys(result.Item) : null;
34
34
  }
35
- // JS-only operator (e.g. `like`) somewhere in `where` — fetch the partition
36
- // and pick the first JS match. Loses the "stop on first hit per page"
37
- // optimization but keeps semantics consistent with `queryMatching`.
38
- if (whereHasJsOnlyOperator(args.where)) {
39
- const all = await queryMatching(adapter, partition, undefined);
40
- for (const row of all){
41
- if (matchesWhere(row, args.where)) return row;
42
- }
43
- return null;
44
- }
45
- const filter = buildFilterExpression(args.where);
35
+ const compiled = buildFilterExpression(where);
46
36
  // Always-false predicate (e.g. `in: []`) → no document can match.
47
- if (filter === null) return null;
48
- let exclusiveStartKey;
49
- while(true){
50
- const result = await docClient.send(new QueryCommand({
51
- TableName: adapter.tableName,
52
- KeyConditionExpression: '#pk = :pk',
53
- ExpressionAttributeNames: {
54
- '#pk': 'pk',
55
- ...filter?.names ?? {}
56
- },
57
- ExpressionAttributeValues: {
58
- ':pk': partition,
59
- ...filter?.values ?? {}
60
- },
61
- ConsistentRead: true,
62
- ...filter ? {
63
- FilterExpression: filter.expression
64
- } : {},
65
- ...exclusiveStartKey ? {
66
- ExclusiveStartKey: exclusiveStartKey
67
- } : {}
68
- }));
69
- for (const item of result.Items ?? []){
70
- return stripInternalKeys(item);
71
- }
72
- if (!result.LastEvaluatedKey) {
73
- return null;
37
+ if (compiled.never) return null;
38
+ for await (const page of partitionPages(adapter, partition, compiled.filter)){
39
+ for (const item of page.items){
40
+ const row = stripInternalKeys(item);
41
+ if (compiled.exact || matchesWhere(row, where)) return row;
74
42
  }
75
- exclusiveStartKey = result.LastEvaluatedKey;
76
43
  }
44
+ return null;
77
45
  }
78
46
 
79
47
  //# sourceMappingURL=findFirst.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/findFirst.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport { GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { buildFilterExpression } from './buildFilterExpression.js'\nimport { matchesWhere } from './matchesWhere.js'\nimport { whereHasJsOnlyOperator } from './operators.js'\nimport { queryMatching } from './queryMatching.js'\nimport { stripInternalKeys } from './stripInternalKeys.js'\nimport { whereToId } from './whereToId.js'\n\n/**\n * Locate the first item in a partition matching `where`. Used by `findOne`\n * directly and by `deleteOne`/`updateOne` to capture the doc before mutating.\n *\n * Fast path: if `where` is a pure id-equality, do a single `GetItem` with the\n * full composite key.\n * Slow path: paginate `Query` (scoped by `pk = partition`) with\n * `FilterExpression` pushdown and stop on the first matching row in any page.\n */\nexport async function findFirst(\n adapter: DynamoAdapter,\n args: { partition: string; where: undefined | Where },\n): Promise<null | Record<string, unknown>> {\n const docClient = adapter.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const { partition } = args\n const id = whereToId(args.where)\n\n if (id !== null) {\n const result = await docClient.send(\n new GetCommand({\n TableName: adapter.tableName,\n Key: { pk: partition, sk: String(id) },\n // Strong consistency — Payload assumes read-after-write visibility.\n ConsistentRead: true,\n }),\n )\n return result.Item ? stripInternalKeys(result.Item) : null\n }\n\n // JS-only operator (e.g. `like`) somewhere in `where` — fetch the partition\n // and pick the first JS match. Loses the \"stop on first hit per page\"\n // optimization but keeps semantics consistent with `queryMatching`.\n if (whereHasJsOnlyOperator(args.where)) {\n const all = await queryMatching(adapter, partition, undefined)\n for (const row of all) {\n if (matchesWhere(row, args.where)) return row\n }\n return null\n }\n\n const filter = buildFilterExpression(args.where)\n // Always-false predicate (e.g. `in: []`) → no document can match.\n if (filter === null) return null\n let exclusiveStartKey: Record<string, unknown> | undefined\n\n while (true) {\n const result = await docClient.send(\n new QueryCommand({\n TableName: adapter.tableName,\n KeyConditionExpression: '#pk = :pk',\n ExpressionAttributeNames: {\n '#pk': 'pk',\n ...(filter?.names ?? {}),\n },\n ExpressionAttributeValues: {\n ':pk': partition,\n ...(filter?.values ?? {}),\n },\n ConsistentRead: true,\n ...(filter ? { FilterExpression: filter.expression } : {}),\n ...(exclusiveStartKey ? { ExclusiveStartKey: exclusiveStartKey } : {}),\n }),\n )\n for (const item of result.Items ?? []) {\n return stripInternalKeys(item)\n }\n if (!result.LastEvaluatedKey) {\n return null\n }\n exclusiveStartKey = result.LastEvaluatedKey\n }\n}\n"],"names":["GetCommand","QueryCommand","buildFilterExpression","matchesWhere","whereHasJsOnlyOperator","queryMatching","stripInternalKeys","whereToId","findFirst","adapter","args","docClient","Error","partition","id","where","result","send","TableName","tableName","Key","pk","sk","String","ConsistentRead","Item","all","undefined","row","filter","exclusiveStartKey","KeyConditionExpression","ExpressionAttributeNames","names","ExpressionAttributeValues","values","FilterExpression","expression","ExclusiveStartKey","item","Items","LastEvaluatedKey"],"mappings":"AAEA,SAASA,UAAU,EAAEC,YAAY,QAAQ,wBAAuB;AAIhE,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,sBAAsB,QAAQ,iBAAgB;AACvD,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,iBAAiB,QAAQ,yBAAwB;AAC1D,SAASC,SAAS,QAAQ,iBAAgB;AAE1C;;;;;;;;CAQC,GACD,OAAO,eAAeC,UACpBC,OAAsB,EACtBC,IAAqD;IAErD,MAAMC,YAAYF,QAAQE,SAAS;IACnC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAM,EAAEC,SAAS,EAAE,GAAGH;IACtB,MAAMI,KAAKP,UAAUG,KAAKK,KAAK;IAE/B,IAAID,OAAO,MAAM;QACf,MAAME,SAAS,MAAML,UAAUM,IAAI,CACjC,IAAIjB,WAAW;YACbkB,WAAWT,QAAQU,SAAS;YAC5BC,KAAK;gBAAEC,IAAIR;gBAAWS,IAAIC,OAAOT;YAAI;YACrC,oEAAoE;YACpEU,gBAAgB;QAClB;QAEF,OAAOR,OAAOS,IAAI,GAAGnB,kBAAkBU,OAAOS,IAAI,IAAI;IACxD;IAEA,4EAA4E;IAC5E,sEAAsE;IACtE,oEAAoE;IACpE,IAAIrB,uBAAuBM,KAAKK,KAAK,GAAG;QACtC,MAAMW,MAAM,MAAMrB,cAAcI,SAASI,WAAWc;QACpD,KAAK,MAAMC,OAAOF,IAAK;YACrB,IAAIvB,aAAayB,KAAKlB,KAAKK,KAAK,GAAG,OAAOa;QAC5C;QACA,OAAO;IACT;IAEA,MAAMC,SAAS3B,sBAAsBQ,KAAKK,KAAK;IAC/C,kEAAkE;IAClE,IAAIc,WAAW,MAAM,OAAO;IAC5B,IAAIC;IAEJ,MAAO,KAAM;QACX,MAAMd,SAAS,MAAML,UAAUM,IAAI,CACjC,IAAIhB,aAAa;YACfiB,WAAWT,QAAQU,SAAS;YAC5BY,wBAAwB;YACxBC,0BAA0B;gBACxB,OAAO;gBACP,GAAIH,QAAQI,SAAS,CAAC,CAAC;YACzB;YACAC,2BAA2B;gBACzB,OAAOrB;gBACP,GAAIgB,QAAQM,UAAU,CAAC,CAAC;YAC1B;YACAX,gBAAgB;YAChB,GAAIK,SAAS;gBAAEO,kBAAkBP,OAAOQ,UAAU;YAAC,IAAI,CAAC,CAAC;YACzD,GAAIP,oBAAoB;gBAAEQ,mBAAmBR;YAAkB,IAAI,CAAC,CAAC;QACvE;QAEF,KAAK,MAAMS,QAAQvB,OAAOwB,KAAK,IAAI,EAAE,CAAE;YACrC,OAAOlC,kBAAkBiC;QAC3B;QACA,IAAI,CAACvB,OAAOyB,gBAAgB,EAAE;YAC5B,OAAO;QACT;QACAX,oBAAoBd,OAAOyB,gBAAgB;IAC7C;AACF"}
1
+ {"version":3,"sources":["../../src/utilities/findFirst.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport { GetCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { buildFilterExpression } from './buildFilterExpression.js'\nimport { matchesWhere } from './matchesWhere.js'\nimport { partitionPages } from './queryPartition.js'\nimport { stripInternalKeys } from './stripInternalKeys.js'\nimport { whereToId } from './whereToId.js'\n\n/**\n * Locate the first item in a partition matching `where`. Used by `findOne`\n * directly and by `deleteOne`/`updateOne` to capture the doc before mutating.\n *\n * Fast path: if `where` is a pure id-equality, do a single `GetItem` with the\n * full composite key.\n * Slow path: walk the partition with as much of `where` pushed down as\n * DynamoDB can express, re-check rows with `matchesWhere` when that pushdown\n * is inexact, and stop on the first match in any page.\n */\nexport async function findFirst(\n adapter: DynamoAdapter,\n args: { partition: string; where: undefined | Where },\n): Promise<null | Record<string, unknown>> {\n const docClient = adapter.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const { partition, where } = args\n const id = whereToId(where)\n\n if (id !== null) {\n const result = await docClient.send(\n new GetCommand({\n TableName: adapter.tableName,\n Key: { pk: partition, sk: String(id) },\n // Strong consistency — Payload assumes read-after-write visibility.\n ConsistentRead: true,\n }),\n )\n return result.Item ? stripInternalKeys(result.Item) : null\n }\n\n const compiled = buildFilterExpression(where)\n // Always-false predicate (e.g. `in: []`) → no document can match.\n if (compiled.never) return null\n\n for await (const page of partitionPages(adapter, partition, compiled.filter)) {\n for (const item of page.items) {\n const row = stripInternalKeys(item)\n if (compiled.exact || matchesWhere(row, where)) return row\n }\n }\n return null\n}\n"],"names":["GetCommand","buildFilterExpression","matchesWhere","partitionPages","stripInternalKeys","whereToId","findFirst","adapter","args","docClient","Error","partition","where","id","result","send","TableName","tableName","Key","pk","sk","String","ConsistentRead","Item","compiled","never","page","filter","item","items","row","exact"],"mappings":"AAEA,SAASA,UAAU,QAAQ,wBAAuB;AAIlD,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,iBAAiB,QAAQ,yBAAwB;AAC1D,SAASC,SAAS,QAAQ,iBAAgB;AAE1C;;;;;;;;;CASC,GACD,OAAO,eAAeC,UACpBC,OAAsB,EACtBC,IAAqD;IAErD,MAAMC,YAAYF,QAAQE,SAAS;IACnC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAM,EAAEC,SAAS,EAAEC,KAAK,EAAE,GAAGJ;IAC7B,MAAMK,KAAKR,UAAUO;IAErB,IAAIC,OAAO,MAAM;QACf,MAAMC,SAAS,MAAML,UAAUM,IAAI,CACjC,IAAIf,WAAW;YACbgB,WAAWT,QAAQU,SAAS;YAC5BC,KAAK;gBAAEC,IAAIR;gBAAWS,IAAIC,OAAOR;YAAI;YACrC,oEAAoE;YACpES,gBAAgB;QAClB;QAEF,OAAOR,OAAOS,IAAI,GAAGnB,kBAAkBU,OAAOS,IAAI,IAAI;IACxD;IAEA,MAAMC,WAAWvB,sBAAsBW;IACvC,kEAAkE;IAClE,IAAIY,SAASC,KAAK,EAAE,OAAO;IAE3B,WAAW,MAAMC,QAAQvB,eAAeI,SAASI,WAAWa,SAASG,MAAM,EAAG;QAC5E,KAAK,MAAMC,QAAQF,KAAKG,KAAK,CAAE;YAC7B,MAAMC,MAAM1B,kBAAkBwB;YAC9B,IAAIJ,SAASO,KAAK,IAAI7B,aAAa4B,KAAKlB,QAAQ,OAAOkB;QACzD;IACF;IACA,OAAO;AACT"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Geometry for Payload's point-field operators (`near`, `within`,
3
+ * `intersects`), evaluated in JS because DynamoDB has no geospatial
4
+ * functions.
5
+ *
6
+ * Points are `[longitude, latitude]` pairs — the shape Payload hands a
7
+ * document-style adapter and the shape we store. A GeoJSON `Point` object is
8
+ * accepted too. Distances are great-circle metres; polygon tests are planar
9
+ * ray casts over lng/lat, which is exact enough for the bounding shapes these
10
+ * operators are used with but is not geodesic.
11
+ */
12
+ type Position = [number, number];
13
+ /** Read a stored or supplied point: `[lng, lat]` or `{ type: 'Point', coordinates }`. */
14
+ export declare function asPosition(value: unknown): null | Position;
15
+ export declare function distanceInMeters(a: Position, b: Position): number;
16
+ export type NearQuery = {
17
+ center: Position;
18
+ maxDistance: null | number;
19
+ minDistance: null | number;
20
+ };
21
+ /**
22
+ * Parse a `near` operand: `'lng,lat,maxDistance,minDistance'` or the same as
23
+ * an array. Distances are metres and optional. Returns `null` when the
24
+ * centre isn't a usable coordinate pair.
25
+ */
26
+ export declare function parseNear(operand: unknown): NearQuery | null;
27
+ export declare function matchesNear(value: unknown, operand: unknown): boolean;
28
+ /** `within`: the stored point lies inside a GeoJSON `Polygon` / `MultiPolygon`. */
29
+ export declare function matchesWithin(value: unknown, operand: unknown): boolean;
30
+ /**
31
+ * `intersects`: for a stored point that means "is this point" against a
32
+ * GeoJSON `Point`, and "lies inside" against a polygon.
33
+ */
34
+ export declare function matchesIntersects(value: unknown, operand: unknown): boolean;
35
+ export {};
36
+ //# sourceMappingURL=geo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geo.d.ts","sourceRoot":"","sources":["../../src/utilities/geo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,KAAK,QAAQ,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAiBhC,yFAAyF;AACzF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,QAAQ,CAS1D;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAQjE;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,WAAW,EAAE,IAAI,GAAG,MAAM,CAAC;IAAC,WAAW,EAAE,IAAI,GAAG,MAAM,CAAA;CAAE,CAAA;AAEpG;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAW5D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAQrE;AAwBD,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CASvE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ3E"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Geometry for Payload's point-field operators (`near`, `within`,
3
+ * `intersects`), evaluated in JS because DynamoDB has no geospatial
4
+ * functions.
5
+ *
6
+ * Points are `[longitude, latitude]` pairs — the shape Payload hands a
7
+ * document-style adapter and the shape we store. A GeoJSON `Point` object is
8
+ * accepted too. Distances are great-circle metres; polygon tests are planar
9
+ * ray casts over lng/lat, which is exact enough for the bounding shapes these
10
+ * operators are used with but is not geodesic.
11
+ */ const EARTH_RADIUS_METERS = 6_371_008.8;
12
+ function isFiniteNumber(value) {
13
+ return typeof value === 'number' && Number.isFinite(value);
14
+ }
15
+ function toNumber(value) {
16
+ if (isFiniteNumber(value)) return value;
17
+ if (typeof value === 'string' && value.trim() !== '') {
18
+ const parsed = Number(value);
19
+ return Number.isFinite(parsed) ? parsed : null;
20
+ }
21
+ return null;
22
+ }
23
+ /** Read a stored or supplied point: `[lng, lat]` or `{ type: 'Point', coordinates }`. */ export function asPosition(value) {
24
+ const pair = value && typeof value === 'object' && !Array.isArray(value) ? value.coordinates : value;
25
+ if (!Array.isArray(pair) || pair.length < 2) return null;
26
+ const lng = toNumber(pair[0]);
27
+ const lat = toNumber(pair[1]);
28
+ return lng === null || lat === null ? null : [
29
+ lng,
30
+ lat
31
+ ];
32
+ }
33
+ export function distanceInMeters(a, b) {
34
+ const toRadians = (degrees)=>degrees * Math.PI / 180;
35
+ const dLat = toRadians(b[1] - a[1]);
36
+ const dLng = toRadians(b[0] - a[0]);
37
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRadians(a[1])) * Math.cos(toRadians(b[1])) * Math.sin(dLng / 2) ** 2;
38
+ return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h)));
39
+ }
40
+ /**
41
+ * Parse a `near` operand: `'lng,lat,maxDistance,minDistance'` or the same as
42
+ * an array. Distances are metres and optional. Returns `null` when the
43
+ * centre isn't a usable coordinate pair.
44
+ */ export function parseNear(operand) {
45
+ const parts = typeof operand === 'string' ? operand.split(',') : operand;
46
+ if (!Array.isArray(parts)) return null;
47
+ const lng = toNumber(parts[0]);
48
+ const lat = toNumber(parts[1]);
49
+ if (lng === null || lat === null) return null;
50
+ return {
51
+ center: [
52
+ lng,
53
+ lat
54
+ ],
55
+ maxDistance: toNumber(parts[2]),
56
+ minDistance: toNumber(parts[3])
57
+ };
58
+ }
59
+ export function matchesNear(value, operand) {
60
+ const point = asPosition(value);
61
+ const query = parseNear(operand);
62
+ if (!point || !query) return false;
63
+ const distance = distanceInMeters(point, query.center);
64
+ if (query.maxDistance !== null && distance > query.maxDistance) return false;
65
+ if (query.minDistance !== null && distance < query.minDistance) return false;
66
+ return true;
67
+ }
68
+ function inRing(point, ring) {
69
+ if (!Array.isArray(ring)) return false;
70
+ let inside = false;
71
+ for(let i = 0, j = ring.length - 1; i < ring.length; j = i++){
72
+ const a = asPosition(ring[i]);
73
+ const b = asPosition(ring[j]);
74
+ if (!a || !b) continue;
75
+ const crosses = a[1] > point[1] !== b[1] > point[1] && point[0] < (b[0] - a[0]) * (point[1] - a[1]) / (b[1] - a[1]) + a[0];
76
+ if (crosses) inside = !inside;
77
+ }
78
+ return inside;
79
+ }
80
+ /** First ring is the outline; any further rings are holes. */ function inPolygon(point, rings) {
81
+ if (!Array.isArray(rings) || rings.length === 0) return false;
82
+ if (!inRing(point, rings[0])) return false;
83
+ return !rings.slice(1).some((hole)=>inRing(point, hole));
84
+ }
85
+ /** `within`: the stored point lies inside a GeoJSON `Polygon` / `MultiPolygon`. */ export function matchesWithin(value, operand) {
86
+ const point = asPosition(value);
87
+ if (!point || !operand || typeof operand !== 'object') return false;
88
+ const { type, coordinates } = operand;
89
+ if (type === 'Polygon') return inPolygon(point, coordinates);
90
+ if (type === 'MultiPolygon') {
91
+ return Array.isArray(coordinates) && coordinates.some((rings)=>inPolygon(point, rings));
92
+ }
93
+ return false;
94
+ }
95
+ /**
96
+ * `intersects`: for a stored point that means "is this point" against a
97
+ * GeoJSON `Point`, and "lies inside" against a polygon.
98
+ */ export function matchesIntersects(value, operand) {
99
+ const point = asPosition(value);
100
+ if (!point || !operand || typeof operand !== 'object') return false;
101
+ if (operand.type === 'Point') {
102
+ const other = asPosition(operand);
103
+ return other !== null && other[0] === point[0] && other[1] === point[1];
104
+ }
105
+ return matchesWithin(value, operand);
106
+ }
107
+
108
+ //# sourceMappingURL=geo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/geo.ts"],"sourcesContent":["/**\n * Geometry for Payload's point-field operators (`near`, `within`,\n * `intersects`), evaluated in JS because DynamoDB has no geospatial\n * functions.\n *\n * Points are `[longitude, latitude]` pairs — the shape Payload hands a\n * document-style adapter and the shape we store. A GeoJSON `Point` object is\n * accepted too. Distances are great-circle metres; polygon tests are planar\n * ray casts over lng/lat, which is exact enough for the bounding shapes these\n * operators are used with but is not geodesic.\n */\n\ntype Position = [number, number]\n\nconst EARTH_RADIUS_METERS = 6_371_008.8\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value)\n}\n\nfunction toNumber(value: unknown): null | number {\n if (isFiniteNumber(value)) return value\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\n/** Read a stored or supplied point: `[lng, lat]` or `{ type: 'Point', coordinates }`. */\nexport function asPosition(value: unknown): null | Position {\n const pair =\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as { coordinates?: unknown }).coordinates\n : value\n if (!Array.isArray(pair) || pair.length < 2) return null\n const lng = toNumber(pair[0])\n const lat = toNumber(pair[1])\n return lng === null || lat === null ? null : [lng, lat]\n}\n\nexport function distanceInMeters(a: Position, b: Position): number {\n const toRadians = (degrees: number) => (degrees * Math.PI) / 180\n const dLat = toRadians(b[1] - a[1])\n const dLng = toRadians(b[0] - a[0])\n const h =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(toRadians(a[1])) * Math.cos(toRadians(b[1])) * Math.sin(dLng / 2) ** 2\n return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h)))\n}\n\nexport type NearQuery = { center: Position; maxDistance: null | number; minDistance: null | number }\n\n/**\n * Parse a `near` operand: `'lng,lat,maxDistance,minDistance'` or the same as\n * an array. Distances are metres and optional. Returns `null` when the\n * centre isn't a usable coordinate pair.\n */\nexport function parseNear(operand: unknown): NearQuery | null {\n const parts = typeof operand === 'string' ? operand.split(',') : operand\n if (!Array.isArray(parts)) return null\n const lng = toNumber(parts[0])\n const lat = toNumber(parts[1])\n if (lng === null || lat === null) return null\n return {\n center: [lng, lat],\n maxDistance: toNumber(parts[2]),\n minDistance: toNumber(parts[3]),\n }\n}\n\nexport function matchesNear(value: unknown, operand: unknown): boolean {\n const point = asPosition(value)\n const query = parseNear(operand)\n if (!point || !query) return false\n const distance = distanceInMeters(point, query.center)\n if (query.maxDistance !== null && distance > query.maxDistance) return false\n if (query.minDistance !== null && distance < query.minDistance) return false\n return true\n}\n\nfunction inRing(point: Position, ring: unknown): boolean {\n if (!Array.isArray(ring)) return false\n let inside = false\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const a = asPosition(ring[i])\n const b = asPosition(ring[j])\n if (!a || !b) continue\n const crosses =\n a[1] > point[1] !== b[1] > point[1] &&\n point[0] < ((b[0] - a[0]) * (point[1] - a[1])) / (b[1] - a[1]) + a[0]\n if (crosses) inside = !inside\n }\n return inside\n}\n\n/** First ring is the outline; any further rings are holes. */\nfunction inPolygon(point: Position, rings: unknown): boolean {\n if (!Array.isArray(rings) || rings.length === 0) return false\n if (!inRing(point, rings[0])) return false\n return !rings.slice(1).some((hole) => inRing(point, hole))\n}\n\n/** `within`: the stored point lies inside a GeoJSON `Polygon` / `MultiPolygon`. */\nexport function matchesWithin(value: unknown, operand: unknown): boolean {\n const point = asPosition(value)\n if (!point || !operand || typeof operand !== 'object') return false\n const { type, coordinates } = operand as { coordinates?: unknown; type?: unknown }\n if (type === 'Polygon') return inPolygon(point, coordinates)\n if (type === 'MultiPolygon') {\n return Array.isArray(coordinates) && coordinates.some((rings) => inPolygon(point, rings))\n }\n return false\n}\n\n/**\n * `intersects`: for a stored point that means \"is this point\" against a\n * GeoJSON `Point`, and \"lies inside\" against a polygon.\n */\nexport function matchesIntersects(value: unknown, operand: unknown): boolean {\n const point = asPosition(value)\n if (!point || !operand || typeof operand !== 'object') return false\n if ((operand as { type?: unknown }).type === 'Point') {\n const other = asPosition(operand)\n return other !== null && other[0] === point[0] && other[1] === point[1]\n }\n return matchesWithin(value, operand)\n}\n"],"names":["EARTH_RADIUS_METERS","isFiniteNumber","value","Number","isFinite","toNumber","trim","parsed","asPosition","pair","Array","isArray","coordinates","length","lng","lat","distanceInMeters","a","b","toRadians","degrees","Math","PI","dLat","dLng","h","sin","cos","asin","min","sqrt","parseNear","operand","parts","split","center","maxDistance","minDistance","matchesNear","point","query","distance","inRing","ring","inside","i","j","crosses","inPolygon","rings","slice","some","hole","matchesWithin","type","matchesIntersects","other"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `Promise.all(items.map(fn))`, but with at most `limit` calls in flight.
3
+ * Results come back in input order.
4
+ *
5
+ * Bulk writes need this: firing one request per row all at once means a
6
+ * `deleteMany` over a few thousand rows opens a few thousand concurrent
7
+ * requests, which exhausts the SDK's socket pool and invites throttling.
8
+ * A rejection stops new work from starting and rejects the whole call, same
9
+ * as `Promise.all`.
10
+ */
11
+ export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
12
+ //# sourceMappingURL=mapWithConcurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapWithConcurrency.d.ts","sourceRoot":"","sources":["../../src/utilities/mapWithConcurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAC3C,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,OAAO,CAAC,CAAC,EAAE,CAAC,CAoBd"}