@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/queryMatching.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/queryMatching.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { buildFilterExpression } from './buildFilterExpression.js'\nimport { matchesWhere } from './matchesWhere.js'\nimport { partitionPages } from './queryPartition.js'\nimport { stripInternalKeys } from './stripInternalKeys.js'\n\n/**\n * Walk a partition, pushing as much of the caller's `where` into\n * `FilterExpression` as DynamoDB can express so non-matching rows are skipped\n * server-side. Returns every matching item materialized in memory — suitable\n * for callers that need the full set (sort, paginate, bulk-mutate).\n *\n * When the pushed-down filter isn't the whole `where` (a JS-only operator\n * such as `like`, or a path through an array), each returned row is\n * re-checked with `matchesWhere`. The expressible clauses still narrow the\n * read, so a `where` that mixes both kinds no longer pulls the entire\n * partition over the wire.\n *\n * Use `findFirst` instead when you only need the first match: that helper\n * stops on the first hit.\n */\nexport async function queryMatching(\n adapter: DynamoAdapter,\n partition: string,\n where: undefined | Where,\n): Promise<Record<string, unknown>[]> {\n const compiled = buildFilterExpression(where)\n // Always-false predicate (e.g. `id: { in: [] }` from access control).\n // Short-circuit so we don't pay for a Query just to return nothing.\n if (compiled.never) return []\n\n const matched: Record<string, unknown>[] = []\n for await (const page of partitionPages(adapter, partition, compiled.filter)) {\n for (const item of page.items) {\n const row = stripInternalKeys(item)\n if (compiled.exact || matchesWhere(row, where)) matched.push(row)\n }\n }\n return matched\n}\n"],"names":["buildFilterExpression","matchesWhere","partitionPages","stripInternalKeys","queryMatching","adapter","partition","where","compiled","never","matched","page","filter","item","items","row","exact","push"],"mappings":"AAIA,SAASA,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,iBAAiB,QAAQ,yBAAwB;AAE1D;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,cACpBC,OAAsB,EACtBC,SAAiB,EACjBC,KAAwB;IAExB,MAAMC,WAAWR,sBAAsBO;IACvC,sEAAsE;IACtE,oEAAoE;IACpE,IAAIC,SAASC,KAAK,EAAE,OAAO,EAAE;IAE7B,MAAMC,UAAqC,EAAE;IAC7C,WAAW,MAAMC,QAAQT,eAAeG,SAASC,WAAWE,SAASI,MAAM,EAAG;QAC5E,KAAK,MAAMC,QAAQF,KAAKG,KAAK,CAAE;YAC7B,MAAMC,MAAMZ,kBAAkBU;YAC9B,IAAIL,SAASQ,KAAK,IAAIf,aAAac,KAAKR,QAAQG,QAAQO,IAAI,CAACF;QAC/D;IACF;IACA,OAAOL;AACT"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DynamoAdapter } from '../types.js';
|
|
2
|
+
import type { FilterExpression } from './buildFilterExpression.js';
|
|
3
|
+
export type PartitionPage = {
|
|
4
|
+
/** Rows on this page that passed the filter (`Count`). */
|
|
5
|
+
count: number;
|
|
6
|
+
/** The rows themselves; empty when `countOnly` is set. */
|
|
7
|
+
items: Record<string, unknown>[];
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Walk one partition (`pk = :partition`) page by page with an optional
|
|
11
|
+
* `FilterExpression`. The single place the adapter issues a `Query`, shared
|
|
12
|
+
* by `queryMatching`, `queryCount` and `findFirst` — each decides what to do
|
|
13
|
+
* with the pages (collect, count, stop at 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
|
+
*
|
|
20
|
+
* With `countOnly`, `Select: 'COUNT'` keeps DynamoDB from shipping item
|
|
21
|
+
* bytes — only the post-filter `Count` per page comes back over the wire.
|
|
22
|
+
*/
|
|
23
|
+
export declare function partitionPages(adapter: DynamoAdapter, partition: string, filter: FilterExpression | undefined, options?: {
|
|
24
|
+
countOnly?: boolean;
|
|
25
|
+
}): AsyncGenerator<PartitionPage>;
|
|
26
|
+
//# sourceMappingURL=queryPartition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryPartition.d.ts","sourceRoot":"","sources":["../../src/utilities/queryPartition.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE,MAAM,MAAM,aAAa,GAAG;IAC1B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CACjC,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAuB,cAAc,CACnC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GACpC,cAAc,CAAC,aAAa,CAAC,CAkC/B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { QueryCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
/**
|
|
3
|
+
* Walk one partition (`pk = :partition`) page by page with an optional
|
|
4
|
+
* `FilterExpression`. The single place the adapter issues a `Query`, shared
|
|
5
|
+
* by `queryMatching`, `queryCount` and `findFirst` — each decides what to do
|
|
6
|
+
* with the pages (collect, count, stop at the first hit).
|
|
7
|
+
*
|
|
8
|
+
* Cost note: `FilterExpression` reduces network bytes but not RCU
|
|
9
|
+
* consumption. DynamoDB still reads every row in the partition before
|
|
10
|
+
* filtering. The next meaningful optimization is GSIs that mirror specific
|
|
11
|
+
* access patterns (e.g. `email` for auth, `slug` for public collections).
|
|
12
|
+
*
|
|
13
|
+
* With `countOnly`, `Select: 'COUNT'` keeps DynamoDB from shipping item
|
|
14
|
+
* bytes — only the post-filter `Count` per page comes back over the wire.
|
|
15
|
+
*/ export async function* partitionPages(adapter, partition, filter, options = {}) {
|
|
16
|
+
const docClient = adapter.docClient;
|
|
17
|
+
if (!docClient) {
|
|
18
|
+
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
19
|
+
}
|
|
20
|
+
let exclusiveStartKey;
|
|
21
|
+
do {
|
|
22
|
+
const result = await docClient.send(new QueryCommand({
|
|
23
|
+
TableName: adapter.tableName,
|
|
24
|
+
KeyConditionExpression: '#pk = :pk',
|
|
25
|
+
ExpressionAttributeNames: {
|
|
26
|
+
'#pk': 'pk',
|
|
27
|
+
...filter?.names ?? {}
|
|
28
|
+
},
|
|
29
|
+
ExpressionAttributeValues: {
|
|
30
|
+
':pk': partition,
|
|
31
|
+
...filter?.values ?? {}
|
|
32
|
+
},
|
|
33
|
+
// Strong consistency by default — Payload assumes read-after-write
|
|
34
|
+
// visibility (Mongo/Postgres do), and DynamoDB's default eventual
|
|
35
|
+
// consistency breaks "create-then-list" flows. 2x RCU but correct.
|
|
36
|
+
ConsistentRead: true,
|
|
37
|
+
...options.countOnly ? {
|
|
38
|
+
Select: 'COUNT'
|
|
39
|
+
} : {},
|
|
40
|
+
...filter ? {
|
|
41
|
+
FilterExpression: filter.expression
|
|
42
|
+
} : {},
|
|
43
|
+
...exclusiveStartKey ? {
|
|
44
|
+
ExclusiveStartKey: exclusiveStartKey
|
|
45
|
+
} : {}
|
|
46
|
+
}));
|
|
47
|
+
yield {
|
|
48
|
+
count: result.Count ?? 0,
|
|
49
|
+
items: result.Items ?? []
|
|
50
|
+
};
|
|
51
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
52
|
+
}while (exclusiveStartKey)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=queryPartition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/queryPartition.ts"],"sourcesContent":["import { QueryCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from '../types.js'\nimport type { FilterExpression } from './buildFilterExpression.js'\n\nexport type PartitionPage = {\n /** Rows on this page that passed the filter (`Count`). */\n count: number\n /** The rows themselves; empty when `countOnly` is set. */\n items: Record<string, unknown>[]\n}\n\n/**\n * Walk one partition (`pk = :partition`) page by page with an optional\n * `FilterExpression`. The single place the adapter issues a `Query`, shared\n * by `queryMatching`, `queryCount` and `findFirst` — each decides what to do\n * with the pages (collect, count, stop at the first hit).\n *\n * Cost note: `FilterExpression` reduces network bytes but not RCU\n * consumption. DynamoDB still reads every row in the partition before\n * filtering. The next meaningful optimization is GSIs that mirror specific\n * access patterns (e.g. `email` for auth, `slug` for public collections).\n *\n * With `countOnly`, `Select: 'COUNT'` keeps DynamoDB from shipping item\n * bytes — only the post-filter `Count` per page comes back over the wire.\n */\nexport async function* partitionPages(\n adapter: DynamoAdapter,\n partition: string,\n filter: FilterExpression | undefined,\n options: { countOnly?: boolean } = {},\n): AsyncGenerator<PartitionPage> {\n const docClient = adapter.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n let exclusiveStartKey: Record<string, unknown> | undefined\n\n do {\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 by default — Payload assumes read-after-write\n // visibility (Mongo/Postgres do), and DynamoDB's default eventual\n // consistency breaks \"create-then-list\" flows. 2x RCU but correct.\n ConsistentRead: true,\n ...(options.countOnly ? { Select: 'COUNT' as const } : {}),\n ...(filter ? { FilterExpression: filter.expression } : {}),\n ...(exclusiveStartKey ? { ExclusiveStartKey: exclusiveStartKey } : {}),\n }),\n )\n\n yield { count: result.Count ?? 0, items: result.Items ?? [] }\n exclusiveStartKey = result.LastEvaluatedKey\n } while (exclusiveStartKey)\n}\n"],"names":["QueryCommand","partitionPages","adapter","partition","filter","options","docClient","Error","exclusiveStartKey","result","send","TableName","tableName","KeyConditionExpression","ExpressionAttributeNames","names","ExpressionAttributeValues","values","ConsistentRead","countOnly","Select","FilterExpression","expression","ExclusiveStartKey","count","Count","items","Items","LastEvaluatedKey"],"mappings":"AAAA,SAASA,YAAY,QAAQ,wBAAuB;AAYpD;;;;;;;;;;;;;CAaC,GACD,OAAO,gBAAgBC,eACrBC,OAAsB,EACtBC,SAAiB,EACjBC,MAAoC,EACpCC,UAAmC,CAAC,CAAC;IAErC,MAAMC,YAAYJ,QAAQI,SAAS;IACnC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,IAAIC;IAEJ,GAAG;QACD,MAAMC,SAAS,MAAMH,UAAUI,IAAI,CACjC,IAAIV,aAAa;YACfW,WAAWT,QAAQU,SAAS;YAC5BC,wBAAwB;YACxBC,0BAA0B;gBACxB,OAAO;gBACP,GAAIV,QAAQW,SAAS,CAAC,CAAC;YACzB;YACAC,2BAA2B;gBACzB,OAAOb;gBACP,GAAIC,QAAQa,UAAU,CAAC,CAAC;YAC1B;YACA,mEAAmE;YACnE,kEAAkE;YAClE,mEAAmE;YACnEC,gBAAgB;YAChB,GAAIb,QAAQc,SAAS,GAAG;gBAAEC,QAAQ;YAAiB,IAAI,CAAC,CAAC;YACzD,GAAIhB,SAAS;gBAAEiB,kBAAkBjB,OAAOkB,UAAU;YAAC,IAAI,CAAC,CAAC;YACzD,GAAId,oBAAoB;gBAAEe,mBAAmBf;YAAkB,IAAI,CAAC,CAAC;QACvE;QAGF,MAAM;YAAEgB,OAAOf,OAAOgB,KAAK,IAAI;YAAGC,OAAOjB,OAAOkB,KAAK,IAAI,EAAE;QAAC;QAC5DnB,oBAAoBC,OAAOmB,gBAAgB;IAC7C,QAASpB,kBAAkB;AAC7B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JoinQuery } from 'payload';
|
|
2
|
+
import type { DynamoAdapter } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fill in `join` fields on documents that have just been read.
|
|
5
|
+
*
|
|
6
|
+
* A join field stores nothing on its own document: `category.posts` is
|
|
7
|
+
* "every post whose `category` points at this category". Payload hands the
|
|
8
|
+
* adapter the joins it wants (`joins`, keyed by join path) and expects each
|
|
9
|
+
* document to come back with
|
|
10
|
+
*
|
|
11
|
+
* { docs: [...ids], hasNextPage: boolean, totalDocs?: number }
|
|
12
|
+
*
|
|
13
|
+
* at that path — ids only; core populates them to the requested depth. For a
|
|
14
|
+
* polymorphic join (`collection: [...]`) each entry is
|
|
15
|
+
* `{ relationTo, value }` instead.
|
|
16
|
+
*
|
|
17
|
+
* Strategy, following the JS join resolution in Payload's Mongo adapter: one
|
|
18
|
+
* query per target collection for *all* parents at once (`on in [parent
|
|
19
|
+
* ids]`, AND-ed with the join's own `where`), then group the results by
|
|
20
|
+
* parent in memory. Sorting, paging and counting are done per parent, so
|
|
21
|
+
* `limit` means "this many per document", as it does with the other
|
|
22
|
+
* adapters' native joins.
|
|
23
|
+
*
|
|
24
|
+
* Only joins named in `joins` with a truthy value run; `joins: false` and
|
|
25
|
+
* `joins[path]: false` leave documents untouched. Joined documents are read
|
|
26
|
+
* from the target's main partition — drafts that were never published are
|
|
27
|
+
* not included.
|
|
28
|
+
*
|
|
29
|
+
* Mutates `docs` in place.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveJoins(adapter: DynamoAdapter, args: {
|
|
32
|
+
collectionSlug: string;
|
|
33
|
+
docs: Record<string, unknown>[];
|
|
34
|
+
joins: JoinQuery | undefined;
|
|
35
|
+
locale?: string | undefined;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
//# sourceMappingURL=resolveJoins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveJoins.d.ts","sourceRoot":"","sources":["../../src/utilities/resolveJoins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,SAAS,CAAA;AAI/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAchD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE;IACJ,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC/B,KAAK,EAAE,SAAS,GAAG,SAAS,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B,GACA,OAAO,CAAC,IAAI,CAAC,CAiHf"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { fieldShouldBeLocalized } from 'payload/shared';
|
|
2
|
+
import { applySorts } from './applySorts.js';
|
|
3
|
+
import { candidatesAt } from './matchesWhere.js';
|
|
4
|
+
import { effectiveLocale, prepareCollectionWhere, prepareSort, resolveStoredPath } from './prepareWhere.js';
|
|
5
|
+
import { queryMatching } from './queryMatching.js';
|
|
6
|
+
/**
|
|
7
|
+
* Fill in `join` fields on documents that have just been read.
|
|
8
|
+
*
|
|
9
|
+
* A join field stores nothing on its own document: `category.posts` is
|
|
10
|
+
* "every post whose `category` points at this category". Payload hands the
|
|
11
|
+
* adapter the joins it wants (`joins`, keyed by join path) and expects each
|
|
12
|
+
* document to come back with
|
|
13
|
+
*
|
|
14
|
+
* { docs: [...ids], hasNextPage: boolean, totalDocs?: number }
|
|
15
|
+
*
|
|
16
|
+
* at that path — ids only; core populates them to the requested depth. For a
|
|
17
|
+
* polymorphic join (`collection: [...]`) each entry is
|
|
18
|
+
* `{ relationTo, value }` instead.
|
|
19
|
+
*
|
|
20
|
+
* Strategy, following the JS join resolution in Payload's Mongo adapter: one
|
|
21
|
+
* query per target collection for *all* parents at once (`on in [parent
|
|
22
|
+
* ids]`, AND-ed with the join's own `where`), then group the results by
|
|
23
|
+
* parent in memory. Sorting, paging and counting are done per parent, so
|
|
24
|
+
* `limit` means "this many per document", as it does with the other
|
|
25
|
+
* adapters' native joins.
|
|
26
|
+
*
|
|
27
|
+
* Only joins named in `joins` with a truthy value run; `joins: false` and
|
|
28
|
+
* `joins[path]: false` leave documents untouched. Joined documents are read
|
|
29
|
+
* from the target's main partition — drafts that were never published are
|
|
30
|
+
* not included.
|
|
31
|
+
*
|
|
32
|
+
* Mutates `docs` in place.
|
|
33
|
+
*/ export async function resolveJoins(adapter, args) {
|
|
34
|
+
const { collectionSlug, docs, joins, locale } = args;
|
|
35
|
+
if (!joins || docs.length === 0) return;
|
|
36
|
+
const collectionConfig = adapter.payload?.collections?.[collectionSlug]?.config;
|
|
37
|
+
if (!collectionConfig) return;
|
|
38
|
+
const definitions = new Map();
|
|
39
|
+
for (const [target, list] of Object.entries(collectionConfig.joins ?? {})){
|
|
40
|
+
for (const join of list)definitions.set(join.joinPath, {
|
|
41
|
+
join,
|
|
42
|
+
targets: [
|
|
43
|
+
target
|
|
44
|
+
]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const join of collectionConfig.polymorphicJoins ?? []){
|
|
48
|
+
const { collection } = join.field;
|
|
49
|
+
definitions.set(join.joinPath, {
|
|
50
|
+
join,
|
|
51
|
+
targets: Array.isArray(collection) ? collection : [
|
|
52
|
+
collection
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const parentIds = docs.map((doc)=>doc['id']);
|
|
57
|
+
const parentKeys = new Set(parentIds.map(String));
|
|
58
|
+
for (const [joinPath, joinQuery] of Object.entries(joins)){
|
|
59
|
+
if (!joinQuery || typeof joinQuery !== 'object') continue;
|
|
60
|
+
const definition = definitions.get(joinPath);
|
|
61
|
+
if (!definition) continue;
|
|
62
|
+
const { join } = definition;
|
|
63
|
+
const query = joinQuery;
|
|
64
|
+
const polymorphicJoin = Array.isArray(join.field.collection);
|
|
65
|
+
const limit = query.limit ?? join.field.defaultLimit ?? 10;
|
|
66
|
+
const skip = (Math.max(1, query.page ?? 1) - 1) * limit;
|
|
67
|
+
const { targets, where: joinWhere } = polymorphicJoin ? narrowTargets(definition.targets, query.where) : {
|
|
68
|
+
targets: definition.targets,
|
|
69
|
+
where: query.where
|
|
70
|
+
};
|
|
71
|
+
// parent key → joined rows, in the order the targets were read.
|
|
72
|
+
const grouped = new Map();
|
|
73
|
+
for (const target of targets){
|
|
74
|
+
const targetConfig = adapter.payload.collections?.[target]?.config;
|
|
75
|
+
if (!targetConfig) continue;
|
|
76
|
+
const on = join.field.on;
|
|
77
|
+
// Look the `on` field up on each target: a polymorphic join has no
|
|
78
|
+
// single target field, and the path may need a locale key.
|
|
79
|
+
const stored = resolveStoredPath(adapter, {
|
|
80
|
+
kind: 'collection',
|
|
81
|
+
slug: target
|
|
82
|
+
}, on, locale);
|
|
83
|
+
const polymorphicTarget = stored.field !== null && 'relationTo' in stored.field && Array.isArray(stored.field.relationTo);
|
|
84
|
+
const pointsAtParents = polymorphicTarget ? {
|
|
85
|
+
[`${on}.relationTo`]: {
|
|
86
|
+
equals: collectionSlug
|
|
87
|
+
},
|
|
88
|
+
[`${on}.value`]: {
|
|
89
|
+
in: parentIds
|
|
90
|
+
}
|
|
91
|
+
} : {
|
|
92
|
+
[on]: {
|
|
93
|
+
in: parentIds
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const rows = await queryMatching(adapter, adapter.resolvePartition(target), await prepareCollectionWhere(adapter, target, joinWhere ? {
|
|
97
|
+
and: [
|
|
98
|
+
joinWhere,
|
|
99
|
+
pointsAtParents
|
|
100
|
+
]
|
|
101
|
+
} : pointsAtParents, locale));
|
|
102
|
+
const sort = query.sort || join.field.defaultSort || targetConfig.defaultSort || '-createdAt';
|
|
103
|
+
applySorts(rows, prepareSort(adapter, {
|
|
104
|
+
kind: 'collection',
|
|
105
|
+
slug: target
|
|
106
|
+
}, sort, locale));
|
|
107
|
+
for (const row of rows){
|
|
108
|
+
// A hasMany `on` field can name several parents; a row joins each of
|
|
109
|
+
// them once.
|
|
110
|
+
const owners = new Set();
|
|
111
|
+
for (const candidate of candidatesAt(row, stored.path)){
|
|
112
|
+
const key = ownerKey(candidate, collectionSlug);
|
|
113
|
+
if (key !== null && parentKeys.has(key)) owners.add(key);
|
|
114
|
+
}
|
|
115
|
+
for (const key of owners){
|
|
116
|
+
const list = grouped.get(key) ?? [];
|
|
117
|
+
list.push({
|
|
118
|
+
relationTo: target,
|
|
119
|
+
row
|
|
120
|
+
});
|
|
121
|
+
grouped.set(key, list);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const joinLocale = fieldShouldBeLocalized({
|
|
126
|
+
field: join.field,
|
|
127
|
+
parentIsLocalized: join.parentIsLocalized
|
|
128
|
+
}) && effectiveLocale(adapter, locale);
|
|
129
|
+
const writePath = joinLocale ? `${joinPath}.${joinLocale}` : joinPath;
|
|
130
|
+
for (const doc of docs){
|
|
131
|
+
const all = grouped.get(String(doc['id'])) ?? [];
|
|
132
|
+
const page = limit === 0 ? all : all.slice(skip, skip + limit);
|
|
133
|
+
setByPath(doc, writePath, {
|
|
134
|
+
docs: page.map(({ relationTo, row })=>polymorphicJoin ? {
|
|
135
|
+
relationTo,
|
|
136
|
+
value: row['id']
|
|
137
|
+
} : row['id']),
|
|
138
|
+
hasNextPage: limit !== 0 && all.length > skip + page.length,
|
|
139
|
+
...query.count ? {
|
|
140
|
+
totalDocs: all.length
|
|
141
|
+
} : {}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The parent a stored `on` value points at, as a string key: a bare id, or
|
|
148
|
+
* the `value` of a polymorphic `{ relationTo, value }` aimed at this
|
|
149
|
+
* collection. `null` for anything else.
|
|
150
|
+
*/ function ownerKey(candidate, collectionSlug) {
|
|
151
|
+
if (typeof candidate === 'string' || typeof candidate === 'number') return String(candidate);
|
|
152
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
153
|
+
const { relationTo, value } = candidate;
|
|
154
|
+
if (relationTo === collectionSlug && (typeof value === 'string' || typeof value === 'number')) {
|
|
155
|
+
return String(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A polymorphic join's `where` may carry a top-level `relationTo` clause to
|
|
162
|
+
* pick which target collections to read. It isn't a field on the targets, so
|
|
163
|
+
* lift it out and return the rest.
|
|
164
|
+
*/ function narrowTargets(targets, where) {
|
|
165
|
+
const clause = where?.['relationTo'];
|
|
166
|
+
if (!where || !clause || typeof clause !== 'object' || Array.isArray(clause)) {
|
|
167
|
+
return {
|
|
168
|
+
targets,
|
|
169
|
+
where
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const { equals, in: within } = clause;
|
|
173
|
+
const wanted = Array.isArray(within) ? within : equals !== undefined ? [
|
|
174
|
+
equals
|
|
175
|
+
] : null;
|
|
176
|
+
const { relationTo: _relationTo, ...rest } = where;
|
|
177
|
+
return {
|
|
178
|
+
targets: wanted ? targets.filter((target)=>wanted.includes(target)) : targets,
|
|
179
|
+
where: Object.keys(rest).length > 0 ? rest : undefined
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function setByPath(target, path, value) {
|
|
183
|
+
const segments = path.split('.');
|
|
184
|
+
let cursor = target;
|
|
185
|
+
for (const segment of segments.slice(0, -1)){
|
|
186
|
+
const next = cursor[segment];
|
|
187
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) {
|
|
188
|
+
cursor[segment] = {};
|
|
189
|
+
}
|
|
190
|
+
cursor = cursor[segment];
|
|
191
|
+
}
|
|
192
|
+
cursor[segments[segments.length - 1]] = value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=resolveJoins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/resolveJoins.ts"],"sourcesContent":["import type { JoinQuery, SanitizedJoins, Where } from 'payload'\n\nimport { fieldShouldBeLocalized } from 'payload/shared'\n\nimport type { DynamoAdapter } from '../types.js'\n\nimport { applySorts } from './applySorts.js'\nimport { candidatesAt } from './matchesWhere.js'\nimport {\n effectiveLocale,\n prepareCollectionWhere,\n prepareSort,\n resolveStoredPath,\n} from './prepareWhere.js'\nimport { queryMatching } from './queryMatching.js'\n\ntype SanitizedJoin = SanitizedJoins[string][number]\n\n/**\n * Fill in `join` fields on documents that have just been read.\n *\n * A join field stores nothing on its own document: `category.posts` is\n * \"every post whose `category` points at this category\". Payload hands the\n * adapter the joins it wants (`joins`, keyed by join path) and expects each\n * document to come back with\n *\n * { docs: [...ids], hasNextPage: boolean, totalDocs?: number }\n *\n * at that path — ids only; core populates them to the requested depth. For a\n * polymorphic join (`collection: [...]`) each entry is\n * `{ relationTo, value }` instead.\n *\n * Strategy, following the JS join resolution in Payload's Mongo adapter: one\n * query per target collection for *all* parents at once (`on in [parent\n * ids]`, AND-ed with the join's own `where`), then group the results by\n * parent in memory. Sorting, paging and counting are done per parent, so\n * `limit` means \"this many per document\", as it does with the other\n * adapters' native joins.\n *\n * Only joins named in `joins` with a truthy value run; `joins: false` and\n * `joins[path]: false` leave documents untouched. Joined documents are read\n * from the target's main partition — drafts that were never published are\n * not included.\n *\n * Mutates `docs` in place.\n */\nexport async function resolveJoins(\n adapter: DynamoAdapter,\n args: {\n collectionSlug: string\n docs: Record<string, unknown>[]\n joins: JoinQuery | undefined\n locale?: string | undefined\n },\n): Promise<void> {\n const { collectionSlug, docs, joins, locale } = args\n if (!joins || docs.length === 0) return\n\n const collectionConfig = adapter.payload?.collections?.[collectionSlug]?.config\n if (!collectionConfig) return\n\n const definitions = new Map<string, { join: SanitizedJoin; targets: string[] }>()\n for (const [target, list] of Object.entries(collectionConfig.joins ?? {})) {\n for (const join of list) definitions.set(join.joinPath, { join, targets: [target] })\n }\n for (const join of collectionConfig.polymorphicJoins ?? []) {\n const { collection } = join.field\n definitions.set(join.joinPath, {\n join,\n targets: Array.isArray(collection) ? collection : [collection],\n })\n }\n\n const parentIds = docs.map((doc) => doc['id'])\n const parentKeys = new Set(parentIds.map(String))\n\n for (const [joinPath, joinQuery] of Object.entries(joins as Record<string, unknown>)) {\n if (!joinQuery || typeof joinQuery !== 'object') continue\n const definition = definitions.get(joinPath)\n if (!definition) continue\n\n const { join } = definition\n const query = joinQuery as {\n count?: boolean\n limit?: number\n page?: number\n sort?: string\n where?: Where\n }\n const polymorphicJoin = Array.isArray(join.field.collection)\n const limit = query.limit ?? join.field.defaultLimit ?? 10\n const skip = (Math.max(1, query.page ?? 1) - 1) * limit\n\n const { targets, where: joinWhere } = polymorphicJoin\n ? narrowTargets(definition.targets, query.where)\n : { targets: definition.targets, where: query.where }\n\n // parent key → joined rows, in the order the targets were read.\n const grouped = new Map<string, { relationTo: string; row: Record<string, unknown> }[]>()\n\n for (const target of targets) {\n const targetConfig = adapter.payload.collections?.[target]?.config\n if (!targetConfig) continue\n\n const on = join.field.on\n // Look the `on` field up on each target: a polymorphic join has no\n // single target field, and the path may need a locale key.\n const stored = resolveStoredPath(adapter, { kind: 'collection', slug: target }, on, locale)\n const polymorphicTarget =\n stored.field !== null &&\n 'relationTo' in stored.field &&\n Array.isArray(stored.field.relationTo)\n const pointsAtParents: Where = polymorphicTarget\n ? {\n [`${on}.relationTo`]: { equals: collectionSlug },\n [`${on}.value`]: { in: parentIds as never },\n }\n : { [on]: { in: parentIds as never } }\n\n const rows = await queryMatching(\n adapter,\n adapter.resolvePartition(target),\n await prepareCollectionWhere(\n adapter,\n target,\n joinWhere ? { and: [joinWhere, pointsAtParents] } : pointsAtParents,\n locale,\n ),\n )\n\n const sort =\n query.sort || join.field.defaultSort || targetConfig.defaultSort || '-createdAt'\n applySorts(rows, prepareSort(adapter, { kind: 'collection', slug: target }, sort, locale))\n\n for (const row of rows) {\n // A hasMany `on` field can name several parents; a row joins each of\n // them once.\n const owners = new Set<string>()\n for (const candidate of candidatesAt(row, stored.path)) {\n const key = ownerKey(candidate, collectionSlug)\n if (key !== null && parentKeys.has(key)) owners.add(key)\n }\n for (const key of owners) {\n const list = grouped.get(key) ?? []\n list.push({ relationTo: target, row })\n grouped.set(key, list)\n }\n }\n }\n\n const joinLocale =\n fieldShouldBeLocalized({ field: join.field, parentIsLocalized: join.parentIsLocalized }) &&\n effectiveLocale(adapter, locale)\n const writePath = joinLocale ? `${joinPath}.${joinLocale}` : joinPath\n\n for (const doc of docs) {\n const all = grouped.get(String(doc['id'])) ?? []\n const page = limit === 0 ? all : all.slice(skip, skip + limit)\n setByPath(doc, writePath, {\n docs: page.map(({ relationTo, row }) =>\n polymorphicJoin ? { relationTo, value: row['id'] } : row['id'],\n ),\n hasNextPage: limit !== 0 && all.length > skip + page.length,\n ...(query.count ? { totalDocs: all.length } : {}),\n })\n }\n }\n}\n\n/**\n * The parent a stored `on` value points at, as a string key: a bare id, or\n * the `value` of a polymorphic `{ relationTo, value }` aimed at this\n * collection. `null` for anything else.\n */\nfunction ownerKey(candidate: unknown, collectionSlug: string): null | string {\n if (typeof candidate === 'string' || typeof candidate === 'number') return String(candidate)\n if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {\n const { relationTo, value } = candidate as { relationTo?: unknown; value?: unknown }\n if (relationTo === collectionSlug && (typeof value === 'string' || typeof value === 'number')) {\n return String(value)\n }\n }\n return null\n}\n\n/**\n * A polymorphic join's `where` may carry a top-level `relationTo` clause to\n * pick which target collections to read. It isn't a field on the targets, so\n * lift it out and return the rest.\n */\nfunction narrowTargets(\n targets: string[],\n where: undefined | Where,\n): { targets: string[]; where: undefined | Where } {\n const clause = where?.['relationTo']\n if (!where || !clause || typeof clause !== 'object' || Array.isArray(clause)) {\n return { targets, where }\n }\n const { equals, in: within } = clause as { equals?: unknown; in?: unknown }\n const wanted = Array.isArray(within) ? within : equals !== undefined ? [equals] : null\n const { relationTo: _relationTo, ...rest } = where\n return {\n targets: wanted ? targets.filter((target) => wanted.includes(target)) : targets,\n where: Object.keys(rest).length > 0 ? (rest as Where) : undefined,\n }\n}\n\nfunction setByPath(target: Record<string, unknown>, path: string, value: unknown): void {\n const segments = path.split('.')\n let cursor = target\n for (const segment of segments.slice(0, -1)) {\n const next = cursor[segment]\n if (!next || typeof next !== 'object' || Array.isArray(next)) {\n cursor[segment] = {}\n }\n cursor = cursor[segment] as Record<string, unknown>\n }\n cursor[segments[segments.length - 1]!] = value\n}\n"],"names":["fieldShouldBeLocalized","applySorts","candidatesAt","effectiveLocale","prepareCollectionWhere","prepareSort","resolveStoredPath","queryMatching","resolveJoins","adapter","args","collectionSlug","docs","joins","locale","length","collectionConfig","payload","collections","config","definitions","Map","target","list","Object","entries","join","set","joinPath","targets","polymorphicJoins","collection","field","Array","isArray","parentIds","map","doc","parentKeys","Set","String","joinQuery","definition","get","query","polymorphicJoin","limit","defaultLimit","skip","Math","max","page","where","joinWhere","narrowTargets","grouped","targetConfig","on","stored","kind","slug","polymorphicTarget","relationTo","pointsAtParents","equals","in","rows","resolvePartition","and","sort","defaultSort","row","owners","candidate","path","key","ownerKey","has","add","push","joinLocale","parentIsLocalized","writePath","all","slice","setByPath","value","hasNextPage","count","totalDocs","clause","within","wanted","undefined","_relationTo","rest","filter","includes","keys","segments","split","cursor","segment","next"],"mappings":"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"}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drop the adapter's internal `pk
|
|
3
|
-
* crosses the boundary back into Payload land. Callers see clean
|
|
4
|
-
* keyed by the user-facing `id`; the composite-key plumbing stays
|
|
2
|
+
* Drop the adapter's internal attributes (`pk`, `sk`, `_rev`) from a row
|
|
3
|
+
* before it crosses the boundary back into Payload land. Callers see clean
|
|
4
|
+
* documents keyed by the user-facing `id`; the composite-key plumbing stays
|
|
5
|
+
* internal.
|
|
6
|
+
*
|
|
7
|
+
* The row's revision is remembered against the returned object — see
|
|
8
|
+
* `revisionOf`.
|
|
5
9
|
*/
|
|
6
10
|
export declare function stripInternalKeys(item: Record<string, unknown>): Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* The revision a row had when it was read, or `undefined` for a row written
|
|
13
|
+
* before revisions existed (or for an object that didn't come from a read).
|
|
14
|
+
*
|
|
15
|
+
* Every write the adapter makes bumps `_rev`. A read-merge-write passes the
|
|
16
|
+
* row it read as `replaces`, and the write only lands if the stored revision
|
|
17
|
+
* is still this one — otherwise someone else wrote in between and the merge
|
|
18
|
+
* has to be redone on the fresh row.
|
|
19
|
+
*/
|
|
20
|
+
export declare function revisionOf(row: object): number | undefined;
|
|
7
21
|
//# sourceMappingURL=stripInternalKeys.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stripInternalKeys.d.ts","sourceRoot":"","sources":["../../src/utilities/stripInternalKeys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stripInternalKeys.d.ts","sourceRoot":"","sources":["../../src/utilities/stripInternalKeys.ts"],"names":[],"mappings":"AAOA;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIxF;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1D"}
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The revision each row object was read at, kept beside the object rather
|
|
3
|
+
* than on it so documents handed back to Payload carry no adapter fields.
|
|
4
|
+
* Entries vanish with the row object.
|
|
5
|
+
*/ const revisions = new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Drop the adapter's internal attributes (`pk`, `sk`, `_rev`) from a row
|
|
8
|
+
* before it crosses the boundary back into Payload land. Callers see clean
|
|
9
|
+
* documents keyed by the user-facing `id`; the composite-key plumbing stays
|
|
10
|
+
* internal.
|
|
11
|
+
*
|
|
12
|
+
* The row's revision is remembered against the returned object — see
|
|
13
|
+
* `revisionOf`.
|
|
5
14
|
*/ export function stripInternalKeys(item) {
|
|
6
|
-
const { pk: _pk, sk: _sk, ...rest } = item;
|
|
15
|
+
const { _rev, pk: _pk, sk: _sk, ...rest } = item;
|
|
16
|
+
if (typeof _rev === 'number') revisions.set(rest, _rev);
|
|
7
17
|
return rest;
|
|
8
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The revision a row had when it was read, or `undefined` for a row written
|
|
21
|
+
* before revisions existed (or for an object that didn't come from a read).
|
|
22
|
+
*
|
|
23
|
+
* Every write the adapter makes bumps `_rev`. A read-merge-write passes the
|
|
24
|
+
* row it read as `replaces`, and the write only lands if the stored revision
|
|
25
|
+
* is still this one — otherwise someone else wrote in between and the merge
|
|
26
|
+
* has to be redone on the fresh row.
|
|
27
|
+
*/ export function revisionOf(row) {
|
|
28
|
+
return revisions.get(row);
|
|
29
|
+
}
|
|
9
30
|
|
|
10
31
|
//# sourceMappingURL=stripInternalKeys.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/stripInternalKeys.ts"],"sourcesContent":["/**\n * Drop the adapter's internal `pk
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/stripInternalKeys.ts"],"sourcesContent":["/**\n * The revision each row object was read at, kept beside the object rather\n * than on it so documents handed back to Payload carry no adapter fields.\n * Entries vanish with the row object.\n */\nconst revisions = new WeakMap<object, number>()\n\n/**\n * Drop the adapter's internal attributes (`pk`, `sk`, `_rev`) from a row\n * before it crosses the boundary back into Payload land. Callers see clean\n * documents keyed by the user-facing `id`; the composite-key plumbing stays\n * internal.\n *\n * The row's revision is remembered against the returned object — see\n * `revisionOf`.\n */\nexport function stripInternalKeys(item: Record<string, unknown>): Record<string, unknown> {\n const { _rev, pk: _pk, sk: _sk, ...rest } = item\n if (typeof _rev === 'number') revisions.set(rest, _rev)\n return rest\n}\n\n/**\n * The revision a row had when it was read, or `undefined` for a row written\n * before revisions existed (or for an object that didn't come from a read).\n *\n * Every write the adapter makes bumps `_rev`. A read-merge-write passes the\n * row it read as `replaces`, and the write only lands if the stored revision\n * is still this one — otherwise someone else wrote in between and the merge\n * has to be redone on the fresh row.\n */\nexport function revisionOf(row: object): number | undefined {\n return revisions.get(row)\n}\n"],"names":["revisions","WeakMap","stripInternalKeys","item","_rev","pk","_pk","sk","_sk","rest","set","revisionOf","row","get"],"mappings":"AAAA;;;;CAIC,GACD,MAAMA,YAAY,IAAIC;AAEtB;;;;;;;;CAQC,GACD,OAAO,SAASC,kBAAkBC,IAA6B;IAC7D,MAAM,EAAEC,IAAI,EAAEC,IAAIC,GAAG,EAAEC,IAAIC,GAAG,EAAE,GAAGC,MAAM,GAAGN;IAC5C,IAAI,OAAOC,SAAS,UAAUJ,UAAUU,GAAG,CAACD,MAAML;IAClD,OAAOK;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,WAAWC,GAAW;IACpC,OAAOZ,UAAUa,GAAG,CAACD;AACvB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DynamoAdapter } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Uniqueness for fields declared `unique: true`.
|
|
4
|
+
*
|
|
5
|
+
* DynamoDB can only enforce uniqueness on a primary key, so each unique
|
|
6
|
+
* value gets a small *guard row* whose key *is* the value:
|
|
7
|
+
*
|
|
8
|
+
* pk = "<collection partition>#unique#<stored field path>"
|
|
9
|
+
* sk = "<type tag>:<value>"
|
|
10
|
+
* owner = "<id of the document holding the value>"
|
|
11
|
+
*
|
|
12
|
+
* A document and its guards are written in one `TransactWriteItems`, each
|
|
13
|
+
* guard conditioned on "no such row, or it's already mine". A second
|
|
14
|
+
* document claiming the same value fails that condition and the whole write
|
|
15
|
+
* is cancelled. Guards live in their own partitions, so they never show up
|
|
16
|
+
* in a collection's reads.
|
|
17
|
+
*
|
|
18
|
+
* Covered: scalar unique fields at the top level or inside groups / named
|
|
19
|
+
* tabs, including localized ones (one guard per locale). Not covered:
|
|
20
|
+
* unique fields inside arrays or blocks, and `hasMany` fields — a list has
|
|
21
|
+
* no single value to key on.
|
|
22
|
+
*
|
|
23
|
+
* Empty values (`null`, `undefined`, `''`) are not guarded: any number of
|
|
24
|
+
* documents may leave a unique field blank.
|
|
25
|
+
*/
|
|
26
|
+
export type UniqueGuard = {
|
|
27
|
+
key: {
|
|
28
|
+
pk: string;
|
|
29
|
+
sk: string;
|
|
30
|
+
};
|
|
31
|
+
/** Schema path of the field, without locale — what a validation error names. */
|
|
32
|
+
path: string;
|
|
33
|
+
};
|
|
34
|
+
/** Whether a collection declares any unique field the adapter guards. */
|
|
35
|
+
export declare function hasUniqueFields(adapter: DynamoAdapter, collectionSlug: string): boolean;
|
|
36
|
+
/** Every guard a document's current values call for. */
|
|
37
|
+
export declare function uniqueGuardsFor(adapter: DynamoAdapter, collectionSlug: string, partition: string, doc: Record<string, unknown>): UniqueGuard[];
|
|
38
|
+
export declare function sameGuard(a: UniqueGuard, b: UniqueGuard): boolean;
|
|
39
|
+
//# sourceMappingURL=uniqueGuards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uniqueGuards.d.ts","sourceRoot":"","sources":["../../src/utilities/uniqueGuards.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAMD,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAavF;AAmBD,wDAAwD;AACxD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,aAAa,EACtB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,WAAW,EAAE,CAuDf;AAuBD,wBAAgB,SAAS,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { fieldShouldBeLocalized } from 'payload/shared';
|
|
3
|
+
const cache = new WeakMap();
|
|
4
|
+
/** Whether a collection declares any unique field the adapter guards. */ export function hasUniqueFields(adapter, collectionSlug) {
|
|
5
|
+
let known = cache.get(adapter);
|
|
6
|
+
if (!known) {
|
|
7
|
+
known = new Map();
|
|
8
|
+
cache.set(adapter, known);
|
|
9
|
+
}
|
|
10
|
+
const cached = known.get(collectionSlug);
|
|
11
|
+
if (cached !== undefined) return cached;
|
|
12
|
+
const fields = adapter.payload?.collections?.[collectionSlug]?.config.flattenedFields;
|
|
13
|
+
const result = fields ? containsUnique(fields) : false;
|
|
14
|
+
known.set(collectionSlug, result);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function isGuardable(field) {
|
|
18
|
+
return 'unique' in field && Boolean(field.unique) && field.name !== 'id' && !('hasMany' in field && field.hasMany);
|
|
19
|
+
}
|
|
20
|
+
function containsUnique(fields) {
|
|
21
|
+
return fields.some((field)=>{
|
|
22
|
+
if (field.type === 'array' || field.type === 'blocks') return false;
|
|
23
|
+
if ('flattenedFields' in field) return containsUnique(field.flattenedFields);
|
|
24
|
+
return isGuardable(field);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Every guard a document's current values call for. */ export function uniqueGuardsFor(adapter, collectionSlug, partition, doc) {
|
|
28
|
+
if (!hasUniqueFields(adapter, collectionSlug)) return [];
|
|
29
|
+
const fields = adapter.payload.collections[collectionSlug].config.flattenedFields;
|
|
30
|
+
const localeCodes = adapter.payload.config.localization ? adapter.payload.config.localization.localeCodes : null;
|
|
31
|
+
const guards = [];
|
|
32
|
+
collect(doc, fields, [], [], false);
|
|
33
|
+
return guards;
|
|
34
|
+
function collect(source, level, schemaPath, storedPath, parentIsLocalized) {
|
|
35
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return;
|
|
36
|
+
const record = source;
|
|
37
|
+
for (const field of level){
|
|
38
|
+
if (field.type === 'array' || field.type === 'blocks') continue;
|
|
39
|
+
const isContainer = 'flattenedFields' in field;
|
|
40
|
+
if (!isContainer && !isGuardable(field)) continue;
|
|
41
|
+
const value = record[field.name];
|
|
42
|
+
const schema = [
|
|
43
|
+
...schemaPath,
|
|
44
|
+
field.name
|
|
45
|
+
];
|
|
46
|
+
const stored = [
|
|
47
|
+
...storedPath,
|
|
48
|
+
field.name
|
|
49
|
+
];
|
|
50
|
+
const localized = Boolean(localeCodes) && fieldShouldBeLocalized({
|
|
51
|
+
field,
|
|
52
|
+
parentIsLocalized
|
|
53
|
+
});
|
|
54
|
+
// A localized field holds one value per locale; visit each.
|
|
55
|
+
const variants = localized && value && typeof value === 'object' && !Array.isArray(value) ? Object.entries(value).filter(([locale])=>localeCodes.includes(locale)).map(([locale, inner])=>[
|
|
56
|
+
inner,
|
|
57
|
+
[
|
|
58
|
+
...stored,
|
|
59
|
+
locale
|
|
60
|
+
]
|
|
61
|
+
]) : localized ? [] : [
|
|
62
|
+
[
|
|
63
|
+
value,
|
|
64
|
+
stored
|
|
65
|
+
]
|
|
66
|
+
];
|
|
67
|
+
for (const [inner, innerStored] of variants){
|
|
68
|
+
if (isContainer) {
|
|
69
|
+
collect(inner, field.flattenedFields, schema, innerStored, parentIsLocalized || localized);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const sk = encodeValue(inner);
|
|
73
|
+
if (sk === null) continue;
|
|
74
|
+
guards.push({
|
|
75
|
+
key: {
|
|
76
|
+
pk: `${partition}#unique#${innerStored.join('.')}`,
|
|
77
|
+
sk
|
|
78
|
+
},
|
|
79
|
+
path: schema.join('.')
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Turn a value into a sort key. The type tag keeps `1` and `'1'` apart;
|
|
87
|
+
* long strings are hashed because a sort key is capped at 1024 bytes.
|
|
88
|
+
* Returns `null` for values that aren't guarded.
|
|
89
|
+
*/ function encodeValue(value) {
|
|
90
|
+
switch(typeof value){
|
|
91
|
+
case 'boolean':
|
|
92
|
+
return `b:${value}`;
|
|
93
|
+
case 'number':
|
|
94
|
+
return Number.isFinite(value) ? `n:${value}` : null;
|
|
95
|
+
case 'string':
|
|
96
|
+
if (value === '') return null;
|
|
97
|
+
return value.length > 256 ? `h:${createHash('sha256').update(value).digest('hex')}` : `s:${value}`;
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function sameGuard(a, b) {
|
|
103
|
+
return a.key.pk === b.key.pk && a.key.sk === b.key.sk;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=uniqueGuards.js.map
|