@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.
- package/README.md +141 -18
- package/dist/count.d.ts.map +1 -1
- package/dist/count.js +4 -2
- package/dist/count.js.map +1 -1
- package/dist/countGlobalVersions.d.ts.map +1 -1
- package/dist/countGlobalVersions.js +6 -1
- package/dist/countGlobalVersions.js.map +1 -1
- package/dist/countVersions.d.ts.map +1 -1
- package/dist/countVersions.js +6 -1
- package/dist/countVersions.js.map +1 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +31 -15
- package/dist/create.js.map +1 -1
- package/dist/createGlobal.d.ts.map +1 -1
- package/dist/createGlobal.js +2 -14
- package/dist/createGlobal.js.map +1 -1
- package/dist/createGlobalVersion.d.ts +1 -1
- package/dist/createGlobalVersion.d.ts.map +1 -1
- package/dist/createGlobalVersion.js +3 -41
- package/dist/createGlobalVersion.js.map +1 -1
- package/dist/createVersion.d.ts +6 -5
- package/dist/createVersion.d.ts.map +1 -1
- package/dist/createVersion.js +8 -48
- package/dist/createVersion.js.map +1 -1
- package/dist/deleteMany.d.ts +1 -6
- package/dist/deleteMany.d.ts.map +1 -1
- package/dist/deleteMany.js +8 -19
- package/dist/deleteMany.js.map +1 -1
- package/dist/deleteOne.d.ts.map +1 -1
- package/dist/deleteOne.js +20 -24
- package/dist/deleteOne.js.map +1 -1
- package/dist/deleteVersions.d.ts.map +1 -1
- package/dist/deleteVersions.js +8 -14
- package/dist/deleteVersions.js.map +1 -1
- package/dist/find.d.ts.map +1 -1
- package/dist/find.js +15 -2
- package/dist/find.js.map +1 -1
- package/dist/findDistinct.d.ts.map +1 -1
- package/dist/findDistinct.js +3 -1
- package/dist/findDistinct.js.map +1 -1
- package/dist/findGlobal.d.ts.map +1 -1
- package/dist/findGlobal.js +3 -2
- package/dist/findGlobal.js.map +1 -1
- package/dist/findGlobalVersions.d.ts.map +1 -1
- package/dist/findGlobalVersions.js +10 -2
- package/dist/findGlobalVersions.js.map +1 -1
- package/dist/findOne.d.ts.map +1 -1
- package/dist/findOne.js +17 -6
- package/dist/findOne.js.map +1 -1
- package/dist/findVersions.d.ts.map +1 -1
- package/dist/findVersions.js +10 -2
- package/dist/findVersions.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/queryDrafts.d.ts.map +1 -1
- package/dist/queryDrafts.js +24 -4
- package/dist/queryDrafts.js.map +1 -1
- package/dist/updateGlobal.d.ts +5 -4
- package/dist/updateGlobal.d.ts.map +1 -1
- package/dist/updateGlobal.js +22 -36
- package/dist/updateGlobal.js.map +1 -1
- package/dist/updateGlobalVersion.d.ts.map +1 -1
- package/dist/updateGlobalVersion.js +3 -49
- package/dist/updateGlobalVersion.js.map +1 -1
- package/dist/updateJobs.d.ts +21 -0
- package/dist/updateJobs.d.ts.map +1 -0
- package/dist/updateJobs.js +83 -0
- package/dist/updateJobs.js.map +1 -0
- package/dist/updateMany.d.ts +9 -3
- package/dist/updateMany.d.ts.map +1 -1
- package/dist/updateMany.js +47 -30
- package/dist/updateMany.js.map +1 -1
- package/dist/updateOne.d.ts +6 -1
- package/dist/updateOne.d.ts.map +1 -1
- package/dist/updateOne.js +49 -49
- package/dist/updateOne.js.map +1 -1
- package/dist/updateVersion.d.ts.map +1 -1
- package/dist/updateVersion.js +3 -52
- package/dist/updateVersion.js.map +1 -1
- package/dist/upsert.d.ts +6 -0
- package/dist/upsert.d.ts.map +1 -1
- package/dist/upsert.js +57 -41
- package/dist/upsert.js.map +1 -1
- package/dist/utilities/backfillUniqueGuards.d.ts +42 -0
- package/dist/utilities/backfillUniqueGuards.d.ts.map +1 -0
- package/dist/utilities/backfillUniqueGuards.js +39 -0
- package/dist/utilities/backfillUniqueGuards.js.map +1 -0
- package/dist/utilities/buildFilterExpression.d.ts +29 -14
- package/dist/utilities/buildFilterExpression.d.ts.map +1 -1
- package/dist/utilities/buildFilterExpression.js +55 -14
- package/dist/utilities/buildFilterExpression.js.map +1 -1
- package/dist/utilities/castWhere.d.ts +84 -0
- package/dist/utilities/castWhere.d.ts.map +1 -0
- package/dist/utilities/castWhere.js +254 -0
- package/dist/utilities/castWhere.js.map +1 -0
- package/dist/utilities/findFirst.d.ts +3 -2
- package/dist/utilities/findFirst.d.ts.map +1 -1
- package/dist/utilities/findFirst.js +14 -46
- package/dist/utilities/findFirst.js.map +1 -1
- package/dist/utilities/geo.d.ts +36 -0
- package/dist/utilities/geo.d.ts.map +1 -0
- package/dist/utilities/geo.js +108 -0
- package/dist/utilities/geo.js.map +1 -0
- package/dist/utilities/mapWithConcurrency.d.ts +12 -0
- package/dist/utilities/mapWithConcurrency.d.ts.map +1 -0
- package/dist/utilities/mapWithConcurrency.js +32 -0
- package/dist/utilities/mapWithConcurrency.js.map +1 -0
- package/dist/utilities/matchesWhere.d.ts +25 -5
- package/dist/utilities/matchesWhere.d.ts.map +1 -1
- package/dist/utilities/matchesWhere.js +150 -38
- package/dist/utilities/matchesWhere.js.map +1 -1
- package/dist/utilities/operators.d.ts +22 -12
- package/dist/utilities/operators.d.ts.map +1 -1
- package/dist/utilities/operators.js +36 -31
- package/dist/utilities/operators.js.map +1 -1
- package/dist/utilities/optimistic.d.ts +29 -0
- package/dist/utilities/optimistic.d.ts.map +1 -0
- package/dist/utilities/optimistic.js +58 -0
- package/dist/utilities/optimistic.js.map +1 -0
- package/dist/utilities/prepareWhere.d.ts +63 -0
- package/dist/utilities/prepareWhere.d.ts.map +1 -0
- package/dist/utilities/prepareWhere.js +198 -0
- package/dist/utilities/prepareWhere.js.map +1 -0
- package/dist/utilities/queryCount.d.ts +6 -3
- package/dist/utilities/queryCount.d.ts.map +1 -1
- package/dist/utilities/queryCount.js +16 -45
- package/dist/utilities/queryCount.js.map +1 -1
- package/dist/utilities/queryMatching.d.ts +8 -7
- package/dist/utilities/queryMatching.d.ts.map +1 -1
- package/dist/utilities/queryMatching.js +17 -54
- package/dist/utilities/queryMatching.js.map +1 -1
- package/dist/utilities/queryPartition.d.ts +26 -0
- package/dist/utilities/queryPartition.d.ts.map +1 -0
- package/dist/utilities/queryPartition.js +55 -0
- package/dist/utilities/queryPartition.js.map +1 -0
- package/dist/utilities/resolveJoins.d.ts +37 -0
- package/dist/utilities/resolveJoins.d.ts.map +1 -0
- package/dist/utilities/resolveJoins.js +195 -0
- package/dist/utilities/resolveJoins.js.map +1 -0
- package/dist/utilities/stripInternalKeys.d.ts +17 -3
- package/dist/utilities/stripInternalKeys.d.ts.map +1 -1
- package/dist/utilities/stripInternalKeys.js +25 -4
- package/dist/utilities/stripInternalKeys.js.map +1 -1
- package/dist/utilities/uniqueGuards.d.ts +39 -0
- package/dist/utilities/uniqueGuards.d.ts.map +1 -0
- package/dist/utilities/uniqueGuards.js +106 -0
- package/dist/utilities/uniqueGuards.js.map +1 -0
- package/dist/utilities/updateVersionRow.d.ts +19 -0
- package/dist/utilities/updateVersionRow.d.ts.map +1 -0
- package/dist/utilities/updateVersionRow.js +40 -0
- package/dist/utilities/updateVersionRow.js.map +1 -0
- package/dist/utilities/writeErrors.d.ts +11 -0
- package/dist/utilities/writeErrors.d.ts.map +1 -0
- package/dist/utilities/writeErrors.js +27 -0
- package/dist/utilities/writeErrors.js.map +1 -0
- package/dist/utilities/writeItems.d.ts +124 -0
- package/dist/utilities/writeItems.d.ts.map +1 -0
- package/dist/utilities/writeItems.js +383 -0
- package/dist/utilities/writeItems.js.map +1 -0
- 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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
5
|
-
*
|
|
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;
|
|
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 {
|
|
3
|
+
import { partitionPages } from './queryPartition.js';
|
|
5
4
|
import { stripInternalKeys } from './stripInternalKeys.js';
|
|
6
5
|
/**
|
|
7
|
-
* Walk a partition
|
|
8
|
-
*
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
}
|