@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,58 @@
1
+ import { GetCommand } from '@aws-sdk/lib-dynamodb';
2
+ import { APIError } from 'payload';
3
+ import { stripInternalKeys } from './stripInternalKeys.js';
4
+ import { isConditionalCheckFailed } from './writeItems.js';
5
+ /**
6
+ * Read one row by id with a strongly consistent `GetItem`. The returned
7
+ * object carries its revision (see `stripInternalKeys`), so it can be handed
8
+ * to `putItem` as `replaces`.
9
+ */ export async function getById(adapter, partition, id) {
10
+ const docClient = adapter.docClient;
11
+ if (!docClient) {
12
+ throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
13
+ }
14
+ const result = await docClient.send(new GetCommand({
15
+ TableName: adapter.tableName,
16
+ Key: {
17
+ pk: partition,
18
+ sk: String(id)
19
+ },
20
+ // Strong consistency — Payload assumes read-after-write visibility.
21
+ ConsistentRead: true
22
+ }));
23
+ return result.Item ? stripInternalKeys(result.Item) : null;
24
+ }
25
+ /** Enough to ride out a burst of writers on one row; beyond it something is wrong. */ export const MAX_WRITE_ATTEMPTS = 10;
26
+ /**
27
+ * Run a read-merge-write until it lands.
28
+ *
29
+ * `write` merges onto the row it's given and puts the result with
30
+ * `replaces: current`. If another writer got in first, the put's revision
31
+ * check fails; `reread` then fetches the row as it is now and the merge is
32
+ * redone on top of it — so two concurrent patches to different fields both
33
+ * survive, instead of the second silently overwriting the first.
34
+ *
35
+ * `reread` returning `null` means the row is gone or no longer qualifies;
36
+ * the result is then `null` and nothing is written.
37
+ */ export async function writeWithRetry(first, reread, write) {
38
+ let current = first;
39
+ for(let attempt = 1; current; attempt++){
40
+ try {
41
+ return await write(current);
42
+ } catch (err) {
43
+ if (!isConditionalCheckFailed(err)) throw err;
44
+ if (attempt >= MAX_WRITE_ATTEMPTS) throw conflictError();
45
+ await backoff(attempt);
46
+ current = await reread();
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ export function conflictError() {
52
+ return new APIError(`payload-ddb: the document kept changing while it was being written; gave up after ${MAX_WRITE_ATTEMPTS} attempts.`, 409, null, true);
53
+ }
54
+ /** A short random pause so writers that collided don't collide again in step. */ export function backoff(attempt) {
55
+ return new Promise((resolve)=>setTimeout(resolve, Math.random() * 5 * attempt));
56
+ }
57
+
58
+ //# sourceMappingURL=optimistic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/optimistic.ts"],"sourcesContent":["import { GetCommand } from '@aws-sdk/lib-dynamodb'\nimport { APIError } from 'payload'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { stripInternalKeys } from './stripInternalKeys.js'\nimport { isConditionalCheckFailed } from './writeItems.js'\n\ntype Row = Record<string, unknown>\n\n/**\n * Read one row by id with a strongly consistent `GetItem`. The returned\n * object carries its revision (see `stripInternalKeys`), so it can be handed\n * to `putItem` as `replaces`.\n */\nexport async function getById(\n adapter: DynamoAdapter,\n partition: string,\n id: unknown,\n): Promise<null | Row> {\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 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/** Enough to ride out a burst of writers on one row; beyond it something is wrong. */\nexport const MAX_WRITE_ATTEMPTS = 10\n\n/**\n * Run a read-merge-write until it lands.\n *\n * `write` merges onto the row it's given and puts the result with\n * `replaces: current`. If another writer got in first, the put's revision\n * check fails; `reread` then fetches the row as it is now and the merge is\n * redone on top of it — so two concurrent patches to different fields both\n * survive, instead of the second silently overwriting the first.\n *\n * `reread` returning `null` means the row is gone or no longer qualifies;\n * the result is then `null` and nothing is written.\n */\nexport async function writeWithRetry<T>(\n first: null | Row,\n reread: () => Promise<null | Row>,\n write: (current: Row) => Promise<T>,\n): Promise<null | T> {\n let current = first\n\n for (let attempt = 1; current; attempt++) {\n try {\n return await write(current)\n } catch (err) {\n if (!isConditionalCheckFailed(err)) throw err\n if (attempt >= MAX_WRITE_ATTEMPTS) throw conflictError()\n await backoff(attempt)\n current = await reread()\n }\n }\n\n return null\n}\n\nexport function conflictError(): APIError {\n return new APIError(\n `payload-ddb: the document kept changing while it was being written; gave up after ${MAX_WRITE_ATTEMPTS} attempts.`,\n 409,\n null,\n true,\n )\n}\n\n/** A short random pause so writers that collided don't collide again in step. */\nexport function backoff(attempt: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, Math.random() * 5 * attempt))\n}\n"],"names":["GetCommand","APIError","stripInternalKeys","isConditionalCheckFailed","getById","adapter","partition","id","docClient","Error","result","send","TableName","tableName","Key","pk","sk","String","ConsistentRead","Item","MAX_WRITE_ATTEMPTS","writeWithRetry","first","reread","write","current","attempt","err","conflictError","backoff","Promise","resolve","setTimeout","Math","random"],"mappings":"AAAA,SAASA,UAAU,QAAQ,wBAAuB;AAClD,SAASC,QAAQ,QAAQ,UAAS;AAIlC,SAASC,iBAAiB,QAAQ,yBAAwB;AAC1D,SAASC,wBAAwB,QAAQ,kBAAiB;AAI1D;;;;CAIC,GACD,OAAO,eAAeC,QACpBC,OAAsB,EACtBC,SAAiB,EACjBC,EAAW;IAEX,MAAMC,YAAYH,QAAQG,SAAS;IACnC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,SAAS,MAAMF,UAAUG,IAAI,CACjC,IAAIX,WAAW;QACbY,WAAWP,QAAQQ,SAAS;QAC5BC,KAAK;YAAEC,IAAIT;YAAWU,IAAIC,OAAOV;QAAI;QACrC,oEAAoE;QACpEW,gBAAgB;IAClB;IAEF,OAAOR,OAAOS,IAAI,GAAGjB,kBAAkBQ,OAAOS,IAAI,IAAI;AACxD;AAEA,oFAAoF,GACpF,OAAO,MAAMC,qBAAqB,GAAE;AAEpC;;;;;;;;;;;CAWC,GACD,OAAO,eAAeC,eACpBC,KAAiB,EACjBC,MAAiC,EACjCC,KAAmC;IAEnC,IAAIC,UAAUH;IAEd,IAAK,IAAII,UAAU,GAAGD,SAASC,UAAW;QACxC,IAAI;YACF,OAAO,MAAMF,MAAMC;QACrB,EAAE,OAAOE,KAAK;YACZ,IAAI,CAACxB,yBAAyBwB,MAAM,MAAMA;YAC1C,IAAID,WAAWN,oBAAoB,MAAMQ;YACzC,MAAMC,QAAQH;YACdD,UAAU,MAAMF;QAClB;IACF;IAEA,OAAO;AACT;AAEA,OAAO,SAASK;IACd,OAAO,IAAI3B,SACT,CAAC,kFAAkF,EAAEmB,mBAAmB,UAAU,CAAC,EACnH,KACA,MACA;AAEJ;AAEA,+EAA+E,GAC/E,OAAO,SAASS,QAAQH,OAAe;IACrC,OAAO,IAAII,QAAQ,CAACC,UAAYC,WAAWD,SAASE,KAAKC,MAAM,KAAK,IAAIR;AAC1E"}
@@ -0,0 +1,63 @@
1
+ import type { Sort, Where } from 'payload';
2
+ import type { DynamoAdapter } from '../types.js';
3
+ import { type ResolvedPath } from './castWhere.js';
4
+ /**
5
+ * Adapter-facing entry points that turn the `where` (and `sort`) Payload
6
+ * hands a method into what the stored rows can answer. Every method that
7
+ * takes a `where` calls one of the `prepare*Where` functions first.
8
+ *
9
+ * Preparation is two passes:
10
+ *
11
+ * 1. **Relationship paths are resolved.** `author.name equals 'x'` can't be
12
+ * answered from a row that only stores the author's id, so the nested
13
+ * part runs as its own query against the related collection and the
14
+ * clause becomes `author in [matching ids]`. Multi-hop paths recurse.
15
+ * This is the same strategy as Payload's Mongo adapter.
16
+ * 2. **`castWhere`** casts operands, inserts locale keys and marks paths the
17
+ * pushdown can't address.
18
+ *
19
+ * Each entry point returns `where` untouched when the slug isn't in the
20
+ * config — same "skip, don't kill the operation" stance as `resolveSchema`.
21
+ */
22
+ /** What a `where` is aimed at: one of the four kinds of partition. */
23
+ export type QueryTarget = {
24
+ kind: 'collection';
25
+ slug: string;
26
+ } | {
27
+ kind: 'collectionVersions';
28
+ slug: string;
29
+ } | {
30
+ kind: 'global';
31
+ slug: string;
32
+ } | {
33
+ kind: 'globalVersions';
34
+ slug: string;
35
+ };
36
+ /** `where` in document terms, against a collection's main partition. */
37
+ export declare function prepareCollectionWhere(adapter: DynamoAdapter, slug: string, where: undefined | Where, locale?: string): Promise<undefined | Where>;
38
+ /** `where` against a global's singleton row. */
39
+ export declare function prepareGlobalWhere(adapter: DynamoAdapter, slug: string, where: undefined | Where, locale?: string): Promise<undefined | Where>;
40
+ /**
41
+ * `where` in version-row terms (`parent`, `latest`, `version.<field>`, …),
42
+ * against the versions partition of a collection or global.
43
+ */
44
+ export declare function prepareVersionsWhere(adapter: DynamoAdapter, parent: {
45
+ kind: 'collection' | 'global';
46
+ slug: string;
47
+ }, where: undefined | Where, locale?: string): Promise<undefined | Where>;
48
+ export declare function prepareWhere(adapter: DynamoAdapter, target: QueryTarget, where: undefined | Where, locale?: string): Promise<undefined | Where>;
49
+ /**
50
+ * Give each sort key its stored path — today that means inserting the locale
51
+ * key after a localized field, so `title` sorts by `title.en` rather than by
52
+ * the `{ en, es }` map it lives in.
53
+ */
54
+ export declare function prepareSort(adapter: DynamoAdapter, target: QueryTarget, sort: Sort | undefined, locale?: string): Sort | undefined;
55
+ /**
56
+ * Look a path up in a target's schema for this request: the field it names,
57
+ * and the path its value is actually stored under (`title` → `title.en`).
58
+ * For callers that read values back out of rows rather than filter on them.
59
+ */
60
+ export declare function resolveStoredPath(adapter: DynamoAdapter, target: QueryTarget, path: string, locale?: string): ResolvedPath;
61
+ /** The locale a request resolves to, or `null` when localization is off. */
62
+ export declare function effectiveLocale(adapter: DynamoAdapter, locale?: string): null | string;
63
+ //# sourceMappingURL=prepareWhere.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepareWhere.d.ts","sourceRoot":"","sources":["../../src/utilities/prepareWhere.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,IAAI,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAI1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,gBAAgB,CAAA;AAGvB;;;;;;;;;;;;;;;;;GAiBG;AAEH,sEAAsE;AACtE,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5C,wEAAwE;AACxE,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,GAAG,KAAK,EACxB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,CAE5B;AAED,gDAAgD;AAChD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,GAAG,KAAK,EACxB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,CAE5B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE;IAAE,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,KAAK,EAAE,SAAS,GAAG,KAAK,EACxB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,CAO5B;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,SAAS,GAAG,KAAK,EACxB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,SAAS,GAAG,KAAK,CAAC,CAK5B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,GAAG,SAAS,CAYlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,GACd,YAAY,CAKd;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAItF"}
@@ -0,0 +1,198 @@
1
+ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload';
2
+ import { castWhere, resolvePath } from './castWhere.js';
3
+ import { queryMatching } from './queryMatching.js';
4
+ /** `where` in document terms, against a collection's main partition. */ export function prepareCollectionWhere(adapter, slug, where, locale) {
5
+ return prepareWhere(adapter, {
6
+ kind: 'collection',
7
+ slug
8
+ }, where, locale);
9
+ }
10
+ /** `where` against a global's singleton row. */ export function prepareGlobalWhere(adapter, slug, where, locale) {
11
+ return prepareWhere(adapter, {
12
+ kind: 'global',
13
+ slug
14
+ }, where, locale);
15
+ }
16
+ /**
17
+ * `where` in version-row terms (`parent`, `latest`, `version.<field>`, …),
18
+ * against the versions partition of a collection or global.
19
+ */ export function prepareVersionsWhere(adapter, parent, where, locale) {
20
+ return prepareWhere(adapter, {
21
+ kind: parent.kind === 'collection' ? 'collectionVersions' : 'globalVersions',
22
+ slug: parent.slug
23
+ }, where, locale);
24
+ }
25
+ export async function prepareWhere(adapter, target, where, locale) {
26
+ if (!where) return where;
27
+ const ctx = contextFor(adapter, target, locale);
28
+ if (!ctx) return where;
29
+ return castWhere(await resolveRelationshipPaths(adapter, where, ctx, locale), ctx);
30
+ }
31
+ /**
32
+ * Give each sort key its stored path — today that means inserting the locale
33
+ * key after a localized field, so `title` sorts by `title.en` rather than by
34
+ * the `{ en, es }` map it lives in.
35
+ */ export function prepareSort(adapter, target, sort, locale) {
36
+ if (!sort) return sort;
37
+ const ctx = contextFor(adapter, target, locale);
38
+ if (!ctx?.localization) return sort;
39
+ const localize = (key)=>{
40
+ if (!key) return key;
41
+ const descending = key.startsWith('-');
42
+ const { path } = resolvePath(ctx.fields, descending ? key.slice(1) : key, ctx.localization);
43
+ return descending ? `-${path}` : path;
44
+ };
45
+ return Array.isArray(sort) ? sort.map(localize) : localize(sort);
46
+ }
47
+ /**
48
+ * Look a path up in a target's schema for this request: the field it names,
49
+ * and the path its value is actually stored under (`title` → `title.en`).
50
+ * For callers that read values back out of rows rather than filter on them.
51
+ */ export function resolveStoredPath(adapter, target, path, locale) {
52
+ const ctx = contextFor(adapter, target, locale);
53
+ return ctx ? resolvePath(ctx.fields, path, ctx.localization) : {
54
+ field: null,
55
+ multi: false,
56
+ path
57
+ };
58
+ }
59
+ /** The locale a request resolves to, or `null` when localization is off. */ export function effectiveLocale(adapter, locale) {
60
+ const localization = adapter.payload?.config?.localization;
61
+ if (!localization) return null;
62
+ return locale && locale !== 'all' && locale !== '*' ? locale : localization.defaultLocale;
63
+ }
64
+ /**
65
+ * Find the first point where a path steps through a relationship into the
66
+ * related collection's own fields. Polymorphic relationships are skipped:
67
+ * their sub-keys (`.value`, `.relationTo`) are stored on the row itself, and
68
+ * a path through them doesn't say which collection to ask.
69
+ */ function findRelationshipHop(fields, path) {
70
+ const segments = path.split('.');
71
+ let current = fields;
72
+ for(let index = 0; index < segments.length - 1; index++){
73
+ const field = current.find((candidate)=>candidate.name === segments[index]);
74
+ if (!field) return null;
75
+ if (field.type === 'relationship' || field.type === 'upload') {
76
+ if (typeof field.relationTo !== 'string') return null;
77
+ return {
78
+ prefix: segments.slice(0, index + 1).join('.'),
79
+ relationTo: field.relationTo,
80
+ rest: segments.slice(index + 1).join('.')
81
+ };
82
+ }
83
+ if ('flattenedFields' in field) {
84
+ current = field.flattenedFields;
85
+ } else if (field.type === 'blocks') {
86
+ current = field.blocks.flatMap((block)=>block.flattenedFields);
87
+ } else {
88
+ return null;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ async function resolveRelationshipPaths(adapter, where, ctx, locale) {
94
+ const out = {};
95
+ const resolved = [];
96
+ for (const [key, raw] of Object.entries(where)){
97
+ if (key === 'and' || key === 'or') {
98
+ out[key] = Array.isArray(raw) ? await Promise.all(raw.map((sub)=>sub && typeof sub === 'object' ? resolveRelationshipPaths(adapter, sub, ctx, locale) : sub)) : raw;
99
+ continue;
100
+ }
101
+ const hop = raw && typeof raw === 'object' && !Array.isArray(raw) ? findRelationshipHop(ctx.fields, key) : null;
102
+ if (!hop) {
103
+ out[key] = raw;
104
+ continue;
105
+ }
106
+ if (hop.rest === 'id') {
107
+ // `author.id` is the value the row already stores under `author`.
108
+ resolved.push({
109
+ [hop.prefix]: raw
110
+ });
111
+ continue;
112
+ }
113
+ const related = await queryMatching(adapter, adapter.resolvePartition(hop.relationTo), // Preparing against the related collection is what makes multi-hop
114
+ // paths (`author.team.name`) work: the next hop resolves there.
115
+ await prepareCollectionWhere(adapter, hop.relationTo, {
116
+ [hop.rest]: raw
117
+ }, locale));
118
+ // No matches leaves `in: []`, which both evaluators read as "match
119
+ // nothing" — the right answer.
120
+ resolved.push({
121
+ [hop.prefix]: {
122
+ in: related.map((doc)=>doc['id'])
123
+ }
124
+ });
125
+ }
126
+ if (resolved.length > 0) {
127
+ // AND-ed alongside the rest, so a resolved clause can't overwrite one
128
+ // the caller put on the relationship field directly.
129
+ out.and = [
130
+ ...Array.isArray(out.and) ? out.and : [],
131
+ ...resolved
132
+ ];
133
+ }
134
+ return out;
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Schema lookup
138
+ // ---------------------------------------------------------------------------
139
+ function idTypeOf(adapter, collectionSlug) {
140
+ return adapter.payload?.collections?.[collectionSlug]?.customIDType ?? adapter.defaultIDType;
141
+ }
142
+ function contextFor(adapter, target, locale) {
143
+ const fields = fieldsFor(adapter, target);
144
+ if (!fields) return null;
145
+ const localization = adapter.payload.config.localization;
146
+ return {
147
+ fields,
148
+ // Only a collection's own rows can have numeric ids; globals are keyed
149
+ // by slug and version rows by UUIDs the adapter generates.
150
+ idType: target.kind === 'collection' ? idTypeOf(adapter, target.slug) : 'text',
151
+ idTypeOf: (related)=>idTypeOf(adapter, related),
152
+ ...localization ? {
153
+ localization: {
154
+ // `all` / `*` ask for every locale at once, which a single stored
155
+ // path can't express; fall back to the default like a missing one.
156
+ locale: effectiveLocale(adapter, locale) ?? localization.defaultLocale,
157
+ localeCodes: localization.localeCodes
158
+ }
159
+ } : {}
160
+ };
161
+ }
162
+ const versionFieldsCache = new WeakMap();
163
+ function fieldsFor(adapter, target) {
164
+ const config = adapter.payload?.config;
165
+ if (!config) return null;
166
+ const collection = ()=>adapter.payload.collections?.[target.slug]?.config;
167
+ const global = ()=>config.globals?.find((candidate)=>candidate.slug === target.slug);
168
+ switch(target.kind){
169
+ case 'collection':
170
+ return collection()?.flattenedFields ?? null;
171
+ case 'global':
172
+ return global()?.flattenedFields ?? null;
173
+ default:
174
+ {
175
+ // Version-row fields are derived, so build them once per slug.
176
+ let cache = versionFieldsCache.get(adapter);
177
+ if (!cache) {
178
+ cache = new Map();
179
+ versionFieldsCache.set(adapter, cache);
180
+ }
181
+ const cacheKey = `${target.kind}:${target.slug}`;
182
+ const cached = cache.get(cacheKey);
183
+ if (cached) return cached;
184
+ let fields = null;
185
+ if (target.kind === 'collectionVersions') {
186
+ const entity = collection();
187
+ if (entity) fields = buildVersionCollectionFields(config, entity, true);
188
+ } else {
189
+ const entity = global();
190
+ if (entity) fields = buildVersionGlobalFields(config, entity, true);
191
+ }
192
+ if (fields) cache.set(cacheKey, fields);
193
+ return fields;
194
+ }
195
+ }
196
+ }
197
+
198
+ //# sourceMappingURL=prepareWhere.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/prepareWhere.ts"],"sourcesContent":["import type { FlattenedField, Sort, Where } from 'payload'\n\nimport { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport {\n type CastContext,\n castWhere,\n type IdType,\n type ResolvedPath,\n resolvePath,\n} from './castWhere.js'\nimport { queryMatching } from './queryMatching.js'\n\n/**\n * Adapter-facing entry points that turn the `where` (and `sort`) Payload\n * hands a method into what the stored rows can answer. Every method that\n * takes a `where` calls one of the `prepare*Where` functions first.\n *\n * Preparation is two passes:\n *\n * 1. **Relationship paths are resolved.** `author.name equals 'x'` can't be\n * answered from a row that only stores the author's id, so the nested\n * part runs as its own query against the related collection and the\n * clause becomes `author in [matching ids]`. Multi-hop paths recurse.\n * This is the same strategy as Payload's Mongo adapter.\n * 2. **`castWhere`** casts operands, inserts locale keys and marks paths the\n * pushdown can't address.\n *\n * Each entry point returns `where` untouched when the slug isn't in the\n * config — same \"skip, don't kill the operation\" stance as `resolveSchema`.\n */\n\n/** What a `where` is aimed at: one of the four kinds of partition. */\nexport type QueryTarget =\n | { kind: 'collection'; slug: string }\n | { kind: 'collectionVersions'; slug: string }\n | { kind: 'global'; slug: string }\n | { kind: 'globalVersions'; slug: string }\n\n/** `where` in document terms, against a collection's main partition. */\nexport function prepareCollectionWhere(\n adapter: DynamoAdapter,\n slug: string,\n where: undefined | Where,\n locale?: string,\n): Promise<undefined | Where> {\n return prepareWhere(adapter, { kind: 'collection', slug }, where, locale)\n}\n\n/** `where` against a global's singleton row. */\nexport function prepareGlobalWhere(\n adapter: DynamoAdapter,\n slug: string,\n where: undefined | Where,\n locale?: string,\n): Promise<undefined | Where> {\n return prepareWhere(adapter, { kind: 'global', slug }, where, locale)\n}\n\n/**\n * `where` in version-row terms (`parent`, `latest`, `version.<field>`, …),\n * against the versions partition of a collection or global.\n */\nexport function prepareVersionsWhere(\n adapter: DynamoAdapter,\n parent: { kind: 'collection' | 'global'; slug: string },\n where: undefined | Where,\n locale?: string,\n): Promise<undefined | Where> {\n return prepareWhere(\n adapter,\n { kind: parent.kind === 'collection' ? 'collectionVersions' : 'globalVersions', slug: parent.slug },\n where,\n locale,\n )\n}\n\nexport async function prepareWhere(\n adapter: DynamoAdapter,\n target: QueryTarget,\n where: undefined | Where,\n locale?: string,\n): Promise<undefined | Where> {\n if (!where) return where\n const ctx = contextFor(adapter, target, locale)\n if (!ctx) return where\n return castWhere(await resolveRelationshipPaths(adapter, where, ctx, locale), ctx)\n}\n\n/**\n * Give each sort key its stored path — today that means inserting the locale\n * key after a localized field, so `title` sorts by `title.en` rather than by\n * the `{ en, es }` map it lives in.\n */\nexport function prepareSort(\n adapter: DynamoAdapter,\n target: QueryTarget,\n sort: Sort | undefined,\n locale?: string,\n): Sort | undefined {\n if (!sort) return sort\n const ctx = contextFor(adapter, target, locale)\n if (!ctx?.localization) return sort\n\n const localize = (key: string): string => {\n if (!key) return key\n const descending = key.startsWith('-')\n const { path } = resolvePath(ctx.fields, descending ? key.slice(1) : key, ctx.localization)\n return descending ? `-${path}` : path\n }\n return Array.isArray(sort) ? sort.map(localize) : localize(sort)\n}\n\n/**\n * Look a path up in a target's schema for this request: the field it names,\n * and the path its value is actually stored under (`title` → `title.en`).\n * For callers that read values back out of rows rather than filter on them.\n */\nexport function resolveStoredPath(\n adapter: DynamoAdapter,\n target: QueryTarget,\n path: string,\n locale?: string,\n): ResolvedPath {\n const ctx = contextFor(adapter, target, locale)\n return ctx\n ? resolvePath(ctx.fields, path, ctx.localization)\n : { field: null, multi: false, path }\n}\n\n/** The locale a request resolves to, or `null` when localization is off. */\nexport function effectiveLocale(adapter: DynamoAdapter, locale?: string): null | string {\n const localization = adapter.payload?.config?.localization\n if (!localization) return null\n return locale && locale !== 'all' && locale !== '*' ? locale : localization.defaultLocale\n}\n\n// ---------------------------------------------------------------------------\n// Relationship paths\n// ---------------------------------------------------------------------------\n\ntype RelationshipHop = {\n /** Path of the relationship field itself, e.g. `author`. */\n prefix: string\n relationTo: string\n /** What's left to ask of the related collection, e.g. `name`. */\n rest: string\n}\n\n/**\n * Find the first point where a path steps through a relationship into the\n * related collection's own fields. Polymorphic relationships are skipped:\n * their sub-keys (`.value`, `.relationTo`) are stored on the row itself, and\n * a path through them doesn't say which collection to ask.\n */\nfunction findRelationshipHop(fields: FlattenedField[], path: string): null | RelationshipHop {\n const segments = path.split('.')\n let current = fields\n\n for (let index = 0; index < segments.length - 1; index++) {\n const field = current.find((candidate) => candidate.name === segments[index])\n if (!field) return null\n\n if (field.type === 'relationship' || field.type === 'upload') {\n if (typeof field.relationTo !== 'string') return null\n return {\n prefix: segments.slice(0, index + 1).join('.'),\n relationTo: field.relationTo,\n rest: segments.slice(index + 1).join('.'),\n }\n }\n\n if ('flattenedFields' in field) {\n current = field.flattenedFields\n } else if (field.type === 'blocks') {\n current = field.blocks.flatMap((block) => block.flattenedFields)\n } else {\n return null\n }\n }\n\n return null\n}\n\nasync function resolveRelationshipPaths(\n adapter: DynamoAdapter,\n where: Where,\n ctx: CastContext,\n locale: string | undefined,\n): Promise<Where> {\n const out: Where = {}\n const resolved: Where[] = []\n\n for (const [key, raw] of Object.entries(where)) {\n if (key === 'and' || key === 'or') {\n out[key] = Array.isArray(raw)\n ? await Promise.all(\n raw.map((sub) =>\n sub && typeof sub === 'object'\n ? resolveRelationshipPaths(adapter, sub, ctx, locale)\n : sub,\n ),\n )\n : (raw as Where[])\n continue\n }\n\n const hop =\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? findRelationshipHop(ctx.fields, key)\n : null\n if (!hop) {\n out[key] = raw as Where[string]\n continue\n }\n\n if (hop.rest === 'id') {\n // `author.id` is the value the row already stores under `author`.\n resolved.push({ [hop.prefix]: raw as Where[string] })\n continue\n }\n\n const related = await queryMatching(\n adapter,\n adapter.resolvePartition(hop.relationTo),\n // Preparing against the related collection is what makes multi-hop\n // paths (`author.team.name`) work: the next hop resolves there.\n await prepareCollectionWhere(\n adapter,\n hop.relationTo,\n { [hop.rest]: raw as Where[string] },\n locale,\n ),\n )\n // No matches leaves `in: []`, which both evaluators read as \"match\n // nothing\" — the right answer.\n resolved.push({ [hop.prefix]: { in: related.map((doc) => doc['id']) as never } })\n }\n\n if (resolved.length > 0) {\n // AND-ed alongside the rest, so a resolved clause can't overwrite one\n // the caller put on the relationship field directly.\n out.and = [...(Array.isArray(out.and) ? out.and : []), ...resolved]\n }\n return out\n}\n\n// ---------------------------------------------------------------------------\n// Schema lookup\n// ---------------------------------------------------------------------------\n\nfunction idTypeOf(adapter: DynamoAdapter, collectionSlug: string): IdType {\n return adapter.payload?.collections?.[collectionSlug]?.customIDType ?? adapter.defaultIDType\n}\n\nfunction contextFor(\n adapter: DynamoAdapter,\n target: QueryTarget,\n locale: string | undefined,\n): CastContext | null {\n const fields = fieldsFor(adapter, target)\n if (!fields) return null\n\n const localization = adapter.payload.config.localization\n return {\n fields,\n // Only a collection's own rows can have numeric ids; globals are keyed\n // by slug and version rows by UUIDs the adapter generates.\n idType: target.kind === 'collection' ? idTypeOf(adapter, target.slug) : 'text',\n idTypeOf: (related) => idTypeOf(adapter, related),\n ...(localization\n ? {\n localization: {\n // `all` / `*` ask for every locale at once, which a single stored\n // path can't express; fall back to the default like a missing one.\n locale: effectiveLocale(adapter, locale) ?? localization.defaultLocale,\n localeCodes: localization.localeCodes,\n },\n }\n : {}),\n }\n}\n\nconst versionFieldsCache = new WeakMap<DynamoAdapter, Map<string, FlattenedField[]>>()\n\nfunction fieldsFor(adapter: DynamoAdapter, target: QueryTarget): FlattenedField[] | null {\n const config = adapter.payload?.config\n if (!config) return null\n\n const collection = () => adapter.payload.collections?.[target.slug]?.config\n const global = () => config.globals?.find((candidate) => candidate.slug === target.slug)\n\n switch (target.kind) {\n case 'collection':\n return collection()?.flattenedFields ?? null\n case 'global':\n return global()?.flattenedFields ?? null\n default: {\n // Version-row fields are derived, so build them once per slug.\n let cache = versionFieldsCache.get(adapter)\n if (!cache) {\n cache = new Map()\n versionFieldsCache.set(adapter, cache)\n }\n const cacheKey = `${target.kind}:${target.slug}`\n const cached = cache.get(cacheKey)\n if (cached) return cached\n\n let fields: FlattenedField[] | null = null\n if (target.kind === 'collectionVersions') {\n const entity = collection()\n if (entity) fields = buildVersionCollectionFields(config, entity, true)\n } else {\n const entity = global()\n if (entity) fields = buildVersionGlobalFields(config, entity, true)\n }\n if (fields) cache.set(cacheKey, fields)\n return fields\n }\n }\n}\n"],"names":["buildVersionCollectionFields","buildVersionGlobalFields","castWhere","resolvePath","queryMatching","prepareCollectionWhere","adapter","slug","where","locale","prepareWhere","kind","prepareGlobalWhere","prepareVersionsWhere","parent","target","ctx","contextFor","resolveRelationshipPaths","prepareSort","sort","localization","localize","key","descending","startsWith","path","fields","slice","Array","isArray","map","resolveStoredPath","field","multi","effectiveLocale","payload","config","defaultLocale","findRelationshipHop","segments","split","current","index","length","find","candidate","name","type","relationTo","prefix","join","rest","flattenedFields","blocks","flatMap","block","out","resolved","raw","Object","entries","Promise","all","sub","hop","push","related","resolvePartition","in","doc","and","idTypeOf","collectionSlug","collections","customIDType","defaultIDType","fieldsFor","idType","localeCodes","versionFieldsCache","WeakMap","collection","global","globals","cache","get","Map","set","cacheKey","cached","entity"],"mappings":"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"}
@@ -1,9 +1,12 @@
1
1
  import type { Where } from 'payload';
2
2
  import type { DynamoAdapter } from '../types.js';
3
3
  /**
4
- * Paginated count over a single partition with optional `FilterExpression`
5
- * pushdown. Uses `Select: 'COUNT'` so DynamoDB doesn't ship item bytes —
6
- * only the post-filter `Count` per page comes back over the wire.
4
+ * Count the rows of a partition matching `where`.
5
+ *
6
+ * When the whole `where` can be pushed down, DynamoDB does the counting
7
+ * (`Select: 'COUNT'`) and no item bytes cross the wire. Otherwise the only
8
+ * correct count is "fetch the candidates and count the ones `matchesWhere`
9
+ * accepts" — which is exactly what `queryMatching` does.
7
10
  */
8
11
  export declare function queryCount(adapter: DynamoAdapter, partition: string, where: undefined | Where): Promise<number>;
9
12
  //# sourceMappingURL=queryCount.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"queryCount.d.ts","sourceRoot":"","sources":["../../src/utilities/queryCount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMhD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,GAAG,KAAK,GACvB,OAAO,CAAC,MAAM,CAAC,CAiDjB"}
1
+ {"version":3,"file":"queryCount.d.ts","sourceRoot":"","sources":["../../src/utilities/queryCount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMhD;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,GAAG,KAAK,GACvB,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
@@ -1,54 +1,25 @@
1
- import { QueryCommand } from '@aws-sdk/lib-dynamodb';
2
1
  import { buildFilterExpression } from './buildFilterExpression.js';
3
- import { whereHasJsOnlyOperator } from './operators.js';
4
2
  import { queryMatching } from './queryMatching.js';
3
+ import { partitionPages } from './queryPartition.js';
5
4
  /**
6
- * Paginated count over a single partition with optional `FilterExpression`
7
- * pushdown. Uses `Select: 'COUNT'` so DynamoDB doesn't ship item bytes —
8
- * only the post-filter `Count` per page comes back over the wire.
5
+ * Count the rows of a partition matching `where`.
6
+ *
7
+ * When the whole `where` can be pushed down, DynamoDB does the counting
8
+ * (`Select: 'COUNT'`) and no item bytes cross the wire. Otherwise the only
9
+ * correct count is "fetch the candidates and count the ones `matchesWhere`
10
+ * accepts" — which is exactly what `queryMatching` does.
9
11
  */ export async function queryCount(adapter, partition, where) {
10
- const docClient = adapter.docClient;
11
- if (!docClient) {
12
- throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
13
- }
14
- // JS-only operators (e.g. `like`) can't be expressed in `FilterExpression`,
15
- // so the only correct count is "fetch and count the matches in memory".
16
- // Loses the `Select: 'COUNT'` byte-savings but keeps semantics consistent
17
- // with `queryMatching`.
18
- if (whereHasJsOnlyOperator(where)) {
19
- const matched = await queryMatching(adapter, partition, where);
20
- return matched.length;
21
- }
22
- const filter = buildFilterExpression(where);
12
+ const compiled = buildFilterExpression(where);
23
13
  // Always-false predicate (e.g. `in: []`) → zero rows without a round-trip.
24
- if (filter === null) return 0;
14
+ if (compiled.never) return 0;
15
+ if (!compiled.exact) {
16
+ return (await queryMatching(adapter, partition, where)).length;
17
+ }
25
18
  let totalDocs = 0;
26
- let exclusiveStartKey;
27
- while(true){
28
- const result = await docClient.send(new QueryCommand({
29
- TableName: adapter.tableName,
30
- KeyConditionExpression: '#pk = :pk',
31
- ExpressionAttributeNames: {
32
- '#pk': 'pk',
33
- ...filter?.names ?? {}
34
- },
35
- ExpressionAttributeValues: {
36
- ':pk': partition,
37
- ...filter?.values ?? {}
38
- },
39
- // Strong consistency — see `queryMatching` for rationale.
40
- ConsistentRead: true,
41
- Select: 'COUNT',
42
- ...filter ? {
43
- FilterExpression: filter.expression
44
- } : {},
45
- ...exclusiveStartKey ? {
46
- ExclusiveStartKey: exclusiveStartKey
47
- } : {}
48
- }));
49
- totalDocs += result.Count ?? 0;
50
- if (!result.LastEvaluatedKey) break;
51
- exclusiveStartKey = result.LastEvaluatedKey;
19
+ for await (const page of partitionPages(adapter, partition, compiled.filter, {
20
+ countOnly: true
21
+ })){
22
+ totalDocs += page.count;
52
23
  }
53
24
  return totalDocs;
54
25
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/queryCount.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport { QueryCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { buildFilterExpression } from './buildFilterExpression.js'\nimport { whereHasJsOnlyOperator } from './operators.js'\nimport { queryMatching } from './queryMatching.js'\n\n/**\n * Paginated count over a single partition with optional `FilterExpression`\n * pushdown. Uses `Select: 'COUNT'` so DynamoDB doesn't ship item bytes —\n * only the post-filter `Count` per page comes back over the wire.\n */\nexport async function queryCount(\n adapter: DynamoAdapter,\n partition: string,\n where: undefined | Where,\n): Promise<number> {\n const docClient = adapter.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n // JS-only operators (e.g. `like`) can't be expressed in `FilterExpression`,\n // so the only correct count is \"fetch and count the matches in memory\".\n // Loses the `Select: 'COUNT'` byte-savings but keeps semantics consistent\n // with `queryMatching`.\n if (whereHasJsOnlyOperator(where)) {\n const matched = await queryMatching(adapter, partition, where)\n return matched.length\n }\n\n const filter = buildFilterExpression(where)\n // Always-false predicate (e.g. `in: []`) → zero rows without a round-trip.\n if (filter === null) return 0\n let totalDocs = 0\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 // Strong consistency — see `queryMatching` for rationale.\n ConsistentRead: true,\n Select: 'COUNT',\n ...(filter ? { FilterExpression: filter.expression } : {}),\n ...(exclusiveStartKey ? { ExclusiveStartKey: exclusiveStartKey } : {}),\n }),\n )\n\n totalDocs += result.Count ?? 0\n\n if (!result.LastEvaluatedKey) break\n exclusiveStartKey = result.LastEvaluatedKey\n }\n\n return totalDocs\n}\n"],"names":["QueryCommand","buildFilterExpression","whereHasJsOnlyOperator","queryMatching","queryCount","adapter","partition","where","docClient","Error","matched","length","filter","totalDocs","exclusiveStartKey","result","send","TableName","tableName","KeyConditionExpression","ExpressionAttributeNames","names","ExpressionAttributeValues","values","ConsistentRead","Select","FilterExpression","expression","ExclusiveStartKey","Count","LastEvaluatedKey"],"mappings":"AAEA,SAASA,YAAY,QAAQ,wBAAuB;AAIpD,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,sBAAsB,QAAQ,iBAAgB;AACvD,SAASC,aAAa,QAAQ,qBAAoB;AAElD;;;;CAIC,GACD,OAAO,eAAeC,WACpBC,OAAsB,EACtBC,SAAiB,EACjBC,KAAwB;IAExB,MAAMC,YAAYH,QAAQG,SAAS;IACnC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,wBAAwB;IACxB,IAAIP,uBAAuBK,QAAQ;QACjC,MAAMG,UAAU,MAAMP,cAAcE,SAASC,WAAWC;QACxD,OAAOG,QAAQC,MAAM;IACvB;IAEA,MAAMC,SAASX,sBAAsBM;IACrC,2EAA2E;IAC3E,IAAIK,WAAW,MAAM,OAAO;IAC5B,IAAIC,YAAY;IAChB,IAAIC;IAEJ,MAAO,KAAM;QACX,MAAMC,SAAS,MAAMP,UAAUQ,IAAI,CACjC,IAAIhB,aAAa;YACfiB,WAAWZ,QAAQa,SAAS;YAC5BC,wBAAwB;YACxBC,0BAA0B;gBACxB,OAAO;gBACP,GAAIR,QAAQS,SAAS,CAAC,CAAC;YACzB;YACAC,2BAA2B;gBACzB,OAAOhB;gBACP,GAAIM,QAAQW,UAAU,CAAC,CAAC;YAC1B;YACA,0DAA0D;YAC1DC,gBAAgB;YAChBC,QAAQ;YACR,GAAIb,SAAS;gBAAEc,kBAAkBd,OAAOe,UAAU;YAAC,IAAI,CAAC,CAAC;YACzD,GAAIb,oBAAoB;gBAAEc,mBAAmBd;YAAkB,IAAI,CAAC,CAAC;QACvE;QAGFD,aAAaE,OAAOc,KAAK,IAAI;QAE7B,IAAI,CAACd,OAAOe,gBAAgB,EAAE;QAC9BhB,oBAAoBC,OAAOe,gBAAgB;IAC7C;IAEA,OAAOjB;AACT"}
1
+ {"version":3,"sources":["../../src/utilities/queryCount.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { buildFilterExpression } from './buildFilterExpression.js'\nimport { queryMatching } from './queryMatching.js'\nimport { partitionPages } from './queryPartition.js'\n\n/**\n * Count the rows of a partition matching `where`.\n *\n * When the whole `where` can be pushed down, DynamoDB does the counting\n * (`Select: 'COUNT'`) and no item bytes cross the wire. Otherwise the only\n * correct count is \"fetch the candidates and count the ones `matchesWhere`\n * accepts\" — which is exactly what `queryMatching` does.\n */\nexport async function queryCount(\n adapter: DynamoAdapter,\n partition: string,\n where: undefined | Where,\n): Promise<number> {\n const compiled = buildFilterExpression(where)\n // Always-false predicate (e.g. `in: []`) → zero rows without a round-trip.\n if (compiled.never) return 0\n\n if (!compiled.exact) {\n return (await queryMatching(adapter, partition, where)).length\n }\n\n let totalDocs = 0\n for await (const page of partitionPages(adapter, partition, compiled.filter, {\n countOnly: true,\n })) {\n totalDocs += page.count\n }\n return totalDocs\n}\n"],"names":["buildFilterExpression","queryMatching","partitionPages","queryCount","adapter","partition","where","compiled","never","exact","length","totalDocs","page","filter","countOnly","count"],"mappings":"AAIA,SAASA,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,cAAc,QAAQ,sBAAqB;AAEpD;;;;;;;CAOC,GACD,OAAO,eAAeC,WACpBC,OAAsB,EACtBC,SAAiB,EACjBC,KAAwB;IAExB,MAAMC,WAAWP,sBAAsBM;IACvC,2EAA2E;IAC3E,IAAIC,SAASC,KAAK,EAAE,OAAO;IAE3B,IAAI,CAACD,SAASE,KAAK,EAAE;QACnB,OAAO,AAAC,CAAA,MAAMR,cAAcG,SAASC,WAAWC,MAAK,EAAGI,MAAM;IAChE;IAEA,IAAIC,YAAY;IAChB,WAAW,MAAMC,QAAQV,eAAeE,SAASC,WAAWE,SAASM,MAAM,EAAE;QAC3EC,WAAW;IACb,GAAI;QACFH,aAAaC,KAAKG,KAAK;IACzB;IACA,OAAOJ;AACT"}
@@ -1,18 +1,19 @@
1
1
  import type { Where } from 'payload';
2
2
  import type { DynamoAdapter } from '../types.js';
3
3
  /**
4
- * Walk a partition via paginated `Query` (`pk = :partition`), pushing the
5
- * caller's `where` into `FilterExpression` so non-matching rows are skipped
4
+ * Walk a partition, pushing as much of the caller's `where` into
5
+ * `FilterExpression` as DynamoDB can express so non-matching rows are skipped
6
6
  * server-side. Returns every matching item materialized in memory — suitable
7
7
  * for callers that need the full set (sort, paginate, bulk-mutate).
8
8
  *
9
+ * When the pushed-down filter isn't the whole `where` (a JS-only operator
10
+ * such as `like`, or a path through an array), each returned row is
11
+ * re-checked with `matchesWhere`. The expressible clauses still narrow the
12
+ * read, so a `where` that mixes both kinds no longer pulls the entire
13
+ * partition over the wire.
14
+ *
9
15
  * Use `findFirst` instead when you only need the first match: that helper
10
16
  * stops on the first hit.
11
- *
12
- * Cost note: `FilterExpression` reduces network bytes but not RCU
13
- * consumption. DynamoDB still reads every row in the partition before
14
- * filtering. The next meaningful optimization is GSIs that mirror specific
15
- * access patterns (e.g. `email` for auth, `slug` for public collections).
16
17
  */
17
18
  export declare function queryMatching(adapter: DynamoAdapter, partition: string, where: undefined | Where): Promise<Record<string, unknown>[]>;
18
19
  //# sourceMappingURL=queryMatching.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"queryMatching.d.ts","sourceRoot":"","sources":["../../src/utilities/queryMatching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAOhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,GAAG,KAAK,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAoDpC"}
1
+ {"version":3,"file":"queryMatching.d.ts","sourceRoot":"","sources":["../../src/utilities/queryMatching.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAOhD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,GAAG,KAAK,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAcpC"}
@@ -1,69 +1,32 @@
1
- import { QueryCommand } from '@aws-sdk/lib-dynamodb';
2
1
  import { buildFilterExpression } from './buildFilterExpression.js';
3
2
  import { matchesWhere } from './matchesWhere.js';
4
- import { whereHasJsOnlyOperator } from './operators.js';
3
+ import { partitionPages } from './queryPartition.js';
5
4
  import { stripInternalKeys } from './stripInternalKeys.js';
6
5
  /**
7
- * Walk a partition via paginated `Query` (`pk = :partition`), pushing the
8
- * caller's `where` into `FilterExpression` so non-matching rows are skipped
6
+ * Walk a partition, pushing as much of the caller's `where` into
7
+ * `FilterExpression` as DynamoDB can express so non-matching rows are skipped
9
8
  * server-side. Returns every matching item materialized in memory — suitable
10
9
  * for callers that need the full set (sort, paginate, bulk-mutate).
11
10
  *
11
+ * When the pushed-down filter isn't the whole `where` (a JS-only operator
12
+ * such as `like`, or a path through an array), each returned row is
13
+ * re-checked with `matchesWhere`. The expressible clauses still narrow the
14
+ * read, so a `where` that mixes both kinds no longer pulls the entire
15
+ * partition over the wire.
16
+ *
12
17
  * Use `findFirst` instead when you only need the first match: that helper
13
18
  * stops on the first hit.
14
- *
15
- * Cost note: `FilterExpression` reduces network bytes but not RCU
16
- * consumption. DynamoDB still reads every row in the partition before
17
- * filtering. The next meaningful optimization is GSIs that mirror specific
18
- * access patterns (e.g. `email` for auth, `slug` for public collections).
19
19
  */ export async function queryMatching(adapter, partition, where) {
20
- const docClient = adapter.docClient;
21
- if (!docClient) {
22
- throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
23
- }
24
- // When `where` uses an operator we can't faithfully push down (today: `like`,
25
- // because Payload's contract is case-insensitive while DynamoDB's
26
- // `contains()` isn't), we fetch the whole partition and evaluate in JS.
27
- // Recurse with `where = undefined` to reuse the standard pagination loop.
28
- if (whereHasJsOnlyOperator(where)) {
29
- const all = await queryMatching(adapter, partition, undefined);
30
- return all.filter((row)=>matchesWhere(row, where));
31
- }
32
- const filter = buildFilterExpression(where);
33
- // `null` signals an always-false predicate (e.g. `id: { in: [] }` from
34
- // access control). Short-circuit so we don't pay for a Query just to
35
- // return nothing.
36
- if (filter === null) return [];
20
+ const compiled = buildFilterExpression(where);
21
+ // Always-false predicate (e.g. `id: { in: [] }` from access control).
22
+ // Short-circuit so we don't pay for a Query just to return nothing.
23
+ if (compiled.never) return [];
37
24
  const matched = [];
38
- let exclusiveStartKey;
39
- while(true){
40
- const result = await docClient.send(new QueryCommand({
41
- TableName: adapter.tableName,
42
- KeyConditionExpression: '#pk = :pk',
43
- ExpressionAttributeNames: {
44
- '#pk': 'pk',
45
- ...filter?.names ?? {}
46
- },
47
- ExpressionAttributeValues: {
48
- ':pk': partition,
49
- ...filter?.values ?? {}
50
- },
51
- // Strong consistency by default — Payload assumes read-after-write
52
- // visibility (Mongo/Postgres do), and DynamoDB's default eventual
53
- // consistency breaks "create-then-list" flows. 2x RCU but correct.
54
- ConsistentRead: true,
55
- ...filter ? {
56
- FilterExpression: filter.expression
57
- } : {},
58
- ...exclusiveStartKey ? {
59
- ExclusiveStartKey: exclusiveStartKey
60
- } : {}
61
- }));
62
- for (const item of result.Items ?? []){
63
- matched.push(stripInternalKeys(item));
25
+ for await (const page of partitionPages(adapter, partition, compiled.filter)){
26
+ for (const item of page.items){
27
+ const row = stripInternalKeys(item);
28
+ if (compiled.exact || matchesWhere(row, where)) matched.push(row);
64
29
  }
65
- if (!result.LastEvaluatedKey) break;
66
- exclusiveStartKey = result.LastEvaluatedKey;
67
30
  }
68
31
  return matched;
69
32
  }