@open-aih/payload-ddb 3.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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/connect.d.ts +3 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +6 -0
- package/dist/connect.js.map +1 -0
- package/dist/count.d.ts +3 -0
- package/dist/count.d.ts.map +1 -0
- package/dist/count.js +9 -0
- package/dist/count.js.map +1 -0
- package/dist/countGlobalVersions.d.ts +3 -0
- package/dist/countGlobalVersions.d.ts.map +1 -0
- package/dist/countGlobalVersions.js +9 -0
- package/dist/countGlobalVersions.js.map +1 -0
- package/dist/countVersions.d.ts +3 -0
- package/dist/countVersions.d.ts.map +1 -0
- package/dist/countVersions.js +9 -0
- package/dist/countVersions.js.map +1 -0
- package/dist/create.d.ts +3 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +38 -0
- package/dist/create.js.map +1 -0
- package/dist/createGlobal.d.ts +8 -0
- package/dist/createGlobal.d.ts.map +1 -0
- package/dist/createGlobal.js +29 -0
- package/dist/createGlobal.js.map +1 -0
- package/dist/createGlobalVersion.d.ts +8 -0
- package/dist/createGlobalVersion.d.ts.map +1 -0
- package/dist/createGlobalVersion.js +80 -0
- package/dist/createGlobalVersion.js.map +1 -0
- package/dist/createVersion.d.ts +18 -0
- package/dist/createVersion.d.ts.map +1 -0
- package/dist/createVersion.js +106 -0
- package/dist/createVersion.js.map +1 -0
- package/dist/deleteMany.d.ts +12 -0
- package/dist/deleteMany.d.ts.map +1 -0
- package/dist/deleteMany.js +27 -0
- package/dist/deleteMany.js.map +1 -0
- package/dist/deleteOne.d.ts +3 -0
- package/dist/deleteOne.d.ts.map +1 -0
- package/dist/deleteOne.js +48 -0
- package/dist/deleteOne.js.map +1 -0
- package/dist/deleteVersions.d.ts +9 -0
- package/dist/deleteVersions.d.ts.map +1 -0
- package/dist/deleteVersions.js +28 -0
- package/dist/deleteVersions.js.map +1 -0
- package/dist/destroy.d.ts +3 -0
- package/dist/destroy.d.ts.map +1 -0
- package/dist/destroy.js +10 -0
- package/dist/destroy.js.map +1 -0
- package/dist/find.d.ts +20 -0
- package/dist/find.d.ts.map +1 -0
- package/dist/find.js +54 -0
- package/dist/find.js.map +1 -0
- package/dist/findDistinct.d.ts +13 -0
- package/dist/findDistinct.d.ts.map +1 -0
- package/dist/findDistinct.js +52 -0
- package/dist/findDistinct.js.map +1 -0
- package/dist/findGlobal.d.ts +11 -0
- package/dist/findGlobal.d.ts.map +1 -0
- package/dist/findGlobal.js +34 -0
- package/dist/findGlobal.js.map +1 -0
- package/dist/findGlobalVersions.d.ts +3 -0
- package/dist/findGlobalVersions.d.ts.map +1 -0
- package/dist/findGlobalVersions.js +32 -0
- package/dist/findGlobalVersions.js.map +1 -0
- package/dist/findOne.d.ts +9 -0
- package/dist/findOne.d.ts.map +1 -0
- package/dist/findOne.js +22 -0
- package/dist/findOne.js.map +1 -0
- package/dist/findVersions.d.ts +10 -0
- package/dist/findVersions.d.ts.map +1 -0
- package/dist/findVersions.js +42 -0
- package/dist/findVersions.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +24 -0
- package/dist/init.js.map +1 -0
- package/dist/queryDrafts.d.ts +3 -0
- package/dist/queryDrafts.d.ts.map +1 -0
- package/dist/queryDrafts.js +103 -0
- package/dist/queryDrafts.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/updateGlobal.d.ts +9 -0
- package/dist/updateGlobal.d.ts.map +1 -0
- package/dist/updateGlobal.js +45 -0
- package/dist/updateGlobal.js.map +1 -0
- package/dist/updateGlobalVersion.d.ts +8 -0
- package/dist/updateGlobalVersion.d.ts.map +1 -0
- package/dist/updateGlobalVersion.js +59 -0
- package/dist/updateGlobalVersion.js.map +1 -0
- package/dist/updateMany.d.ts +13 -0
- package/dist/updateMany.d.ts.map +1 -0
- package/dist/updateMany.js +48 -0
- package/dist/updateMany.js.map +1 -0
- package/dist/updateOne.d.ts +13 -0
- package/dist/updateOne.d.ts.map +1 -0
- package/dist/updateOne.js +68 -0
- package/dist/updateOne.js.map +1 -0
- package/dist/updateVersion.d.ts +15 -0
- package/dist/updateVersion.d.ts.map +1 -0
- package/dist/updateVersion.js +69 -0
- package/dist/updateVersion.js.map +1 -0
- package/dist/upsert.d.ts +10 -0
- package/dist/upsert.d.ts.map +1 -0
- package/dist/upsert.js +55 -0
- package/dist/upsert.js.map +1 -0
- package/dist/utilities/applySorts.d.ts +11 -0
- package/dist/utilities/applySorts.d.ts.map +1 -0
- package/dist/utilities/applySorts.js +47 -0
- package/dist/utilities/applySorts.js.map +1 -0
- package/dist/utilities/buildFilterExpression.d.ts +42 -0
- package/dist/utilities/buildFilterExpression.d.ts.map +1 -0
- package/dist/utilities/buildFilterExpression.js +137 -0
- package/dist/utilities/buildFilterExpression.js.map +1 -0
- package/dist/utilities/draftsFallback.d.ts +28 -0
- package/dist/utilities/draftsFallback.d.ts.map +1 -0
- package/dist/utilities/draftsFallback.js +52 -0
- package/dist/utilities/draftsFallback.js.map +1 -0
- package/dist/utilities/ensureConnected.d.ts +12 -0
- package/dist/utilities/ensureConnected.d.ts.map +1 -0
- package/dist/utilities/ensureConnected.js +20 -0
- package/dist/utilities/ensureConnected.js.map +1 -0
- package/dist/utilities/ensureTable.d.ts +16 -0
- package/dist/utilities/ensureTable.d.ts.map +1 -0
- package/dist/utilities/ensureTable.js +62 -0
- package/dist/utilities/ensureTable.js.map +1 -0
- package/dist/utilities/findFirst.d.ts +16 -0
- package/dist/utilities/findFirst.d.ts.map +1 -0
- package/dist/utilities/findFirst.js +79 -0
- package/dist/utilities/findFirst.js.map +1 -0
- package/dist/utilities/getByPath.d.ts +9 -0
- package/dist/utilities/getByPath.d.ts.map +1 -0
- package/dist/utilities/getByPath.js +22 -0
- package/dist/utilities/getByPath.js.map +1 -0
- package/dist/utilities/matchesWhere.d.ts +12 -0
- package/dist/utilities/matchesWhere.d.ts.map +1 -0
- package/dist/utilities/matchesWhere.js +98 -0
- package/dist/utilities/matchesWhere.js.map +1 -0
- package/dist/utilities/normalizeForDynamo.d.ts +15 -0
- package/dist/utilities/normalizeForDynamo.d.ts.map +1 -0
- package/dist/utilities/normalizeForDynamo.js +27 -0
- package/dist/utilities/normalizeForDynamo.js.map +1 -0
- package/dist/utilities/operators.d.ts +26 -0
- package/dist/utilities/operators.d.ts.map +1 -0
- package/dist/utilities/operators.js +58 -0
- package/dist/utilities/operators.js.map +1 -0
- package/dist/utilities/pickConfiguredFields.d.ts +10 -0
- package/dist/utilities/pickConfiguredFields.d.ts.map +1 -0
- package/dist/utilities/pickConfiguredFields.js +113 -0
- package/dist/utilities/pickConfiguredFields.js.map +1 -0
- package/dist/utilities/queryCount.d.ts +9 -0
- package/dist/utilities/queryCount.d.ts.map +1 -0
- package/dist/utilities/queryCount.js +56 -0
- package/dist/utilities/queryCount.js.map +1 -0
- package/dist/utilities/queryMatching.d.ts +18 -0
- package/dist/utilities/queryMatching.d.ts.map +1 -0
- package/dist/utilities/queryMatching.js +71 -0
- package/dist/utilities/queryMatching.js.map +1 -0
- package/dist/utilities/resolveSchema.d.ts +56 -0
- package/dist/utilities/resolveSchema.d.ts.map +1 -0
- package/dist/utilities/resolveSchema.js +84 -0
- package/dist/utilities/resolveSchema.js.map +1 -0
- package/dist/utilities/scrubUnknownFields.d.ts +44 -0
- package/dist/utilities/scrubUnknownFields.d.ts.map +1 -0
- package/dist/utilities/scrubUnknownFields.js +88 -0
- package/dist/utilities/scrubUnknownFields.js.map +1 -0
- package/dist/utilities/stripInternalKeys.d.ts +7 -0
- package/dist/utilities/stripInternalKeys.d.ts.map +1 -0
- package/dist/utilities/stripInternalKeys.js +10 -0
- package/dist/utilities/stripInternalKeys.js.map +1 -0
- package/dist/utilities/whereToId.d.ts +11 -0
- package/dist/utilities/whereToId.d.ts.map +1 -0
- package/dist/utilities/whereToId.js +38 -0
- package/dist/utilities/whereToId.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { applySorts } from './utilities/applySorts.js';
|
|
3
|
+
import { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
|
|
4
|
+
import { queryMatching } from './utilities/queryMatching.js';
|
|
5
|
+
import { projectForCollection } from './utilities/resolveSchema.js';
|
|
6
|
+
/**
|
|
7
|
+
* v1 strategy: query-and-collect matches, sort, slice to `limit`, then
|
|
8
|
+
* read-merge-write each in parallel. Same merge semantics as `updateOne`:
|
|
9
|
+
* `data` is overlaid on the target and `id` is preserved from the target.
|
|
10
|
+
*
|
|
11
|
+
* `Promise.all` is order-preserving, so the returned array matches the
|
|
12
|
+
* sorted target order. Bulk DynamoDB transact-writes (`TransactWriteItems`,
|
|
13
|
+
* 100-item / 4 MB cap) are the natural next optimization but require the
|
|
14
|
+
* transaction wiring we deliberately stubbed out for now.
|
|
15
|
+
*/ export const updateMany = async function updateMany({ collection, data, limit, returning, sort, where }) {
|
|
16
|
+
const docClient = this.docClient;
|
|
17
|
+
if (!docClient) {
|
|
18
|
+
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
19
|
+
}
|
|
20
|
+
const partition = this.resolvePartition(collection);
|
|
21
|
+
const matched = await queryMatching(this, partition, where);
|
|
22
|
+
applySorts(matched, sort);
|
|
23
|
+
const targets = limit && limit > 0 ? matched.slice(0, limit) : matched;
|
|
24
|
+
// Hoisted so every row in the batch shares one timestamp — keeps audit-style
|
|
25
|
+
// queries deterministic. Explicit `updatedAt` in `data` still wins.
|
|
26
|
+
const updatedAt = data['updatedAt'] ?? new Date().toISOString();
|
|
27
|
+
const updated = await Promise.all(targets.map(async (target)=>{
|
|
28
|
+
const merged = {
|
|
29
|
+
...target,
|
|
30
|
+
...data,
|
|
31
|
+
id: target['id'],
|
|
32
|
+
updatedAt
|
|
33
|
+
};
|
|
34
|
+
const projected = projectForCollection(this, collection, merged);
|
|
35
|
+
await docClient.send(new PutCommand({
|
|
36
|
+
TableName: this.tableName,
|
|
37
|
+
Item: normalizeForDynamo({
|
|
38
|
+
...projected,
|
|
39
|
+
pk: partition,
|
|
40
|
+
sk: String(projected['id'])
|
|
41
|
+
})
|
|
42
|
+
}));
|
|
43
|
+
return projected;
|
|
44
|
+
}));
|
|
45
|
+
return returning === false ? null : updated;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=updateMany.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/updateMany.ts"],"sourcesContent":["import type { UpdateMany } from 'payload'\n\nimport { PutCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { applySorts } from './utilities/applySorts.js'\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { queryMatching } from './utilities/queryMatching.js'\nimport { projectForCollection } from './utilities/resolveSchema.js'\n\n/**\n * v1 strategy: query-and-collect matches, sort, slice to `limit`, then\n * read-merge-write each in parallel. Same merge semantics as `updateOne`:\n * `data` is overlaid on the target and `id` is preserved from the target.\n *\n * `Promise.all` is order-preserving, so the returned array matches the\n * sorted target order. Bulk DynamoDB transact-writes (`TransactWriteItems`,\n * 100-item / 4 MB cap) are the natural next optimization but require the\n * transaction wiring we deliberately stubbed out for now.\n */\nexport const updateMany: UpdateMany = async function updateMany(\n this: DynamoAdapter,\n { collection, data, limit, returning, sort, where },\n) {\n const docClient = this.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const partition = this.resolvePartition(collection)\n const matched = await queryMatching(this, partition, where)\n applySorts(matched, sort)\n\n const targets = limit && limit > 0 ? matched.slice(0, limit) : matched\n\n // Hoisted so every row in the batch shares one timestamp — keeps audit-style\n // queries deterministic. Explicit `updatedAt` in `data` still wins.\n const updatedAt = data['updatedAt'] ?? new Date().toISOString()\n\n const updated = await Promise.all(\n targets.map(async (target) => {\n const merged: Record<string, unknown> = {\n ...target,\n ...data,\n id: target['id'],\n updatedAt,\n }\n const projected = projectForCollection(this, collection, merged)\n await docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: normalizeForDynamo({\n ...projected,\n pk: partition,\n sk: String(projected['id']),\n }),\n }),\n )\n return projected\n }),\n )\n\n return returning === false ? null : updated\n}\n"],"names":["PutCommand","applySorts","normalizeForDynamo","queryMatching","projectForCollection","updateMany","collection","data","limit","returning","sort","where","docClient","Error","partition","resolvePartition","matched","targets","slice","updatedAt","Date","toISOString","updated","Promise","all","map","target","merged","id","projected","send","TableName","tableName","Item","pk","sk","String"],"mappings":"AAEA,SAASA,UAAU,QAAQ,wBAAuB;AAIlD,SAASC,UAAU,QAAQ,4BAA2B;AACtD,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,aAAa,QAAQ,+BAA8B;AAC5D,SAASC,oBAAoB,QAAQ,+BAA8B;AAEnE;;;;;;;;;CASC,GACD,OAAO,MAAMC,aAAyB,eAAeA,WAEnD,EAAEC,UAAU,EAAEC,IAAI,EAAEC,KAAK,EAAEC,SAAS,EAAEC,IAAI,EAAEC,KAAK,EAAE;IAEnD,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,YAAY,IAAI,CAACC,gBAAgB,CAACT;IACxC,MAAMU,UAAU,MAAMb,cAAc,IAAI,EAAEW,WAAWH;IACrDV,WAAWe,SAASN;IAEpB,MAAMO,UAAUT,SAASA,QAAQ,IAAIQ,QAAQE,KAAK,CAAC,GAAGV,SAASQ;IAE/D,6EAA6E;IAC7E,oEAAoE;IACpE,MAAMG,YAAYZ,IAAI,CAAC,YAAY,IAAI,IAAIa,OAAOC,WAAW;IAE7D,MAAMC,UAAU,MAAMC,QAAQC,GAAG,CAC/BP,QAAQQ,GAAG,CAAC,OAAOC;QACjB,MAAMC,SAAkC;YACtC,GAAGD,MAAM;YACT,GAAGnB,IAAI;YACPqB,IAAIF,MAAM,CAAC,KAAK;YAChBP;QACF;QACA,MAAMU,YAAYzB,qBAAqB,IAAI,EAAEE,YAAYqB;QACzD,MAAMf,UAAUkB,IAAI,CAClB,IAAI9B,WAAW;YACb+B,WAAW,IAAI,CAACC,SAAS;YACzBC,MAAM/B,mBAAmB;gBACvB,GAAG2B,SAAS;gBACZK,IAAIpB;gBACJqB,IAAIC,OAAOP,SAAS,CAAC,KAAK;YAC5B;QACF;QAEF,OAAOA;IACT;IAGF,OAAOpB,cAAc,QAAQ,OAAOa;AACtC,EAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UpdateOne } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* v1 strategy: read-merge-write rather than building an `UpdateExpression`.
|
|
4
|
+
* Payload sends a partial `data` patch; DynamoDB `UpdateItem` would let us
|
|
5
|
+
* push the diff server-side, but its expression syntax (reserved words,
|
|
6
|
+
* nested attribute paths, list indexing, removal semantics) is enough work to
|
|
7
|
+
* earn its own milestone. Two round-trips is acceptable for now.
|
|
8
|
+
*
|
|
9
|
+
* `id` on the resulting item always comes from the located target — we don't
|
|
10
|
+
* let `data` rewrite the primary key, which would silently orphan the row.
|
|
11
|
+
*/
|
|
12
|
+
export declare const updateOne: UpdateOne;
|
|
13
|
+
//# sourceMappingURL=updateOne.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateOne.d.ts","sourceRoot":"","sources":["../src/updateOne.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAWxC;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,EAAE,SAwDvB,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { findFirst } from './utilities/findFirst.js';
|
|
3
|
+
import { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
|
|
4
|
+
import { projectForCollection } from './utilities/resolveSchema.js';
|
|
5
|
+
import { stripInternalKeys } from './utilities/stripInternalKeys.js';
|
|
6
|
+
/**
|
|
7
|
+
* v1 strategy: read-merge-write rather than building an `UpdateExpression`.
|
|
8
|
+
* Payload sends a partial `data` patch; DynamoDB `UpdateItem` would let us
|
|
9
|
+
* push the diff server-side, but its expression syntax (reserved words,
|
|
10
|
+
* nested attribute paths, list indexing, removal semantics) is enough work to
|
|
11
|
+
* earn its own milestone. Two round-trips is acceptable for now.
|
|
12
|
+
*
|
|
13
|
+
* `id` on the resulting item always comes from the located target — we don't
|
|
14
|
+
* let `data` rewrite the primary key, which would silently orphan the row.
|
|
15
|
+
*/ export const updateOne = async function updateOne(args) {
|
|
16
|
+
const docClient = this.docClient;
|
|
17
|
+
if (!docClient) {
|
|
18
|
+
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
19
|
+
}
|
|
20
|
+
const partition = this.resolvePartition(args.collection);
|
|
21
|
+
let target = null;
|
|
22
|
+
if (args.id !== undefined && args.id !== null) {
|
|
23
|
+
const result = await docClient.send(new GetCommand({
|
|
24
|
+
TableName: this.tableName,
|
|
25
|
+
Key: {
|
|
26
|
+
pk: partition,
|
|
27
|
+
sk: String(args.id)
|
|
28
|
+
},
|
|
29
|
+
ConsistentRead: true
|
|
30
|
+
}));
|
|
31
|
+
target = result.Item ? stripInternalKeys(result.Item) : null;
|
|
32
|
+
} else if (args.where) {
|
|
33
|
+
target = await findFirst(this, {
|
|
34
|
+
partition,
|
|
35
|
+
where: args.where
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!target) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const merged = {
|
|
42
|
+
...target,
|
|
43
|
+
...args.data,
|
|
44
|
+
id: target['id'],
|
|
45
|
+
// Bump `updatedAt` automatically. Explicit timestamps in `data` (e.g. from
|
|
46
|
+
// version restores or migrations) still win because they land in the
|
|
47
|
+
// spread above and we only override here when `data` didn't supply one.
|
|
48
|
+
updatedAt: args.data['updatedAt'] ?? new Date().toISOString()
|
|
49
|
+
};
|
|
50
|
+
// Strip the merged result against the collection schema. Catches both
|
|
51
|
+
// unknown keys in the incoming patch and any pre-existing leaked keys on
|
|
52
|
+
// the persisted target — every update doubles as incremental cleanup.
|
|
53
|
+
const projected = projectForCollection(this, args.collection, merged);
|
|
54
|
+
await docClient.send(new PutCommand({
|
|
55
|
+
TableName: this.tableName,
|
|
56
|
+
Item: normalizeForDynamo({
|
|
57
|
+
...projected,
|
|
58
|
+
pk: partition,
|
|
59
|
+
sk: String(projected['id'])
|
|
60
|
+
})
|
|
61
|
+
}));
|
|
62
|
+
if (args.returning === false) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return projected;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=updateOne.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/updateOne.ts"],"sourcesContent":["import type { UpdateOne } from 'payload'\n\nimport { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { findFirst } from './utilities/findFirst.js'\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { projectForCollection } from './utilities/resolveSchema.js'\nimport { stripInternalKeys } from './utilities/stripInternalKeys.js'\n\n/**\n * v1 strategy: read-merge-write rather than building an `UpdateExpression`.\n * Payload sends a partial `data` patch; DynamoDB `UpdateItem` would let us\n * push the diff server-side, but its expression syntax (reserved words,\n * nested attribute paths, list indexing, removal semantics) is enough work to\n * earn its own milestone. Two round-trips is acceptable for now.\n *\n * `id` on the resulting item always comes from the located target — we don't\n * let `data` rewrite the primary key, which would silently orphan the row.\n */\nexport const updateOne: UpdateOne = async function updateOne(this: DynamoAdapter, args) {\n const docClient = this.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const partition = this.resolvePartition(args.collection)\n\n let target: null | Record<string, unknown> = null\n\n if (args.id !== undefined && args.id !== null) {\n const result = await docClient.send(\n new GetCommand({\n TableName: this.tableName,\n Key: { pk: partition, sk: String(args.id) },\n ConsistentRead: true,\n }),\n )\n target = result.Item ? stripInternalKeys(result.Item) : null\n } else if (args.where) {\n target = await findFirst(this, { partition, where: args.where })\n }\n\n if (!target) {\n return null as never\n }\n\n const merged: Record<string, unknown> = {\n ...target,\n ...args.data,\n id: target['id'],\n // Bump `updatedAt` automatically. Explicit timestamps in `data` (e.g. from\n // version restores or migrations) still win because they land in the\n // spread above and we only override here when `data` didn't supply one.\n updatedAt: args.data['updatedAt'] ?? new Date().toISOString(),\n }\n // Strip the merged result against the collection schema. Catches both\n // unknown keys in the incoming patch and any pre-existing leaked keys on\n // the persisted target — every update doubles as incremental cleanup.\n const projected = projectForCollection(this, args.collection, merged)\n\n await docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: normalizeForDynamo({\n ...projected,\n pk: partition,\n sk: String(projected['id']),\n }),\n }),\n )\n\n if (args.returning === false) {\n return null as never\n }\n return projected as never\n}\n"],"names":["GetCommand","PutCommand","findFirst","normalizeForDynamo","projectForCollection","stripInternalKeys","updateOne","args","docClient","Error","partition","resolvePartition","collection","target","id","undefined","result","send","TableName","tableName","Key","pk","sk","String","ConsistentRead","Item","where","merged","data","updatedAt","Date","toISOString","projected","returning"],"mappings":"AAEA,SAASA,UAAU,EAAEC,UAAU,QAAQ,wBAAuB;AAI9D,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,oBAAoB,QAAQ,+BAA8B;AACnE,SAASC,iBAAiB,QAAQ,mCAAkC;AAEpE;;;;;;;;;CASC,GACD,OAAO,MAAMC,YAAuB,eAAeA,UAA+BC,IAAI;IACpF,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,YAAY,IAAI,CAACC,gBAAgB,CAACJ,KAAKK,UAAU;IAEvD,IAAIC,SAAyC;IAE7C,IAAIN,KAAKO,EAAE,KAAKC,aAAaR,KAAKO,EAAE,KAAK,MAAM;QAC7C,MAAME,SAAS,MAAMR,UAAUS,IAAI,CACjC,IAAIjB,WAAW;YACbkB,WAAW,IAAI,CAACC,SAAS;YACzBC,KAAK;gBAAEC,IAAIX;gBAAWY,IAAIC,OAAOhB,KAAKO,EAAE;YAAE;YAC1CU,gBAAgB;QAClB;QAEFX,SAASG,OAAOS,IAAI,GAAGpB,kBAAkBW,OAAOS,IAAI,IAAI;IAC1D,OAAO,IAAIlB,KAAKmB,KAAK,EAAE;QACrBb,SAAS,MAAMX,UAAU,IAAI,EAAE;YAAEQ;YAAWgB,OAAOnB,KAAKmB,KAAK;QAAC;IAChE;IAEA,IAAI,CAACb,QAAQ;QACX,OAAO;IACT;IAEA,MAAMc,SAAkC;QACtC,GAAGd,MAAM;QACT,GAAGN,KAAKqB,IAAI;QACZd,IAAID,MAAM,CAAC,KAAK;QAChB,2EAA2E;QAC3E,qEAAqE;QACrE,wEAAwE;QACxEgB,WAAWtB,KAAKqB,IAAI,CAAC,YAAY,IAAI,IAAIE,OAAOC,WAAW;IAC7D;IACA,sEAAsE;IACtE,yEAAyE;IACzE,sEAAsE;IACtE,MAAMC,YAAY5B,qBAAqB,IAAI,EAAEG,KAAKK,UAAU,EAAEe;IAE9D,MAAMnB,UAAUS,IAAI,CAClB,IAAIhB,WAAW;QACbiB,WAAW,IAAI,CAACC,SAAS;QACzBM,MAAMtB,mBAAmB;YACvB,GAAG6B,SAAS;YACZX,IAAIX;YACJY,IAAIC,OAAOS,SAAS,CAAC,KAAK;QAC5B;IACF;IAGF,IAAIzB,KAAK0B,SAAS,KAAK,OAAO;QAC5B,OAAO;IACT;IACA,OAAOD;AACT,EAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UpdateVersion } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Read-merge-write against the versions partition. `versionData` is
|
|
4
|
+
* splatted onto the existing row, which means Payload-passed metadata
|
|
5
|
+
* (`createdAt`, `updatedAt`, `latest`, `parent`, `publishedLocale`) and the
|
|
6
|
+
* inner `version` payload all overlay correctly.
|
|
7
|
+
*
|
|
8
|
+
* The adapter does not cascade `latest=true` flips — Payload is responsible
|
|
9
|
+
* for clearing the previous latest by issuing a separate `updateVersion`
|
|
10
|
+
* call. This matches the `createVersion` boundary: `createVersion` owns the
|
|
11
|
+
* cascade because it knows it's making a new latest; `updateVersion` writes
|
|
12
|
+
* exactly what it's told.
|
|
13
|
+
*/
|
|
14
|
+
export declare const updateVersion: UpdateVersion;
|
|
15
|
+
//# sourceMappingURL=updateVersion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateVersion.d.ts","sourceRoot":"","sources":["../src/updateVersion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAW5C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,EAAE,aA2D3B,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { findFirst } from './utilities/findFirst.js';
|
|
3
|
+
import { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
|
|
4
|
+
import { projectVersionRow } from './utilities/resolveSchema.js';
|
|
5
|
+
import { stripInternalKeys } from './utilities/stripInternalKeys.js';
|
|
6
|
+
/**
|
|
7
|
+
* Read-merge-write against the versions partition. `versionData` is
|
|
8
|
+
* splatted onto the existing row, which means Payload-passed metadata
|
|
9
|
+
* (`createdAt`, `updatedAt`, `latest`, `parent`, `publishedLocale`) and the
|
|
10
|
+
* inner `version` payload all overlay correctly.
|
|
11
|
+
*
|
|
12
|
+
* The adapter does not cascade `latest=true` flips — Payload is responsible
|
|
13
|
+
* for clearing the previous latest by issuing a separate `updateVersion`
|
|
14
|
+
* call. This matches the `createVersion` boundary: `createVersion` owns the
|
|
15
|
+
* cascade because it knows it's making a new latest; `updateVersion` writes
|
|
16
|
+
* exactly what it's told.
|
|
17
|
+
*/ export const updateVersion = async function updateVersion(args) {
|
|
18
|
+
const docClient = this.docClient;
|
|
19
|
+
if (!docClient) {
|
|
20
|
+
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
21
|
+
}
|
|
22
|
+
const partition = this.resolveVersionsPartition(args.collection);
|
|
23
|
+
let target = null;
|
|
24
|
+
if (args.id !== undefined && args.id !== null) {
|
|
25
|
+
const result = await docClient.send(new GetCommand({
|
|
26
|
+
TableName: this.tableName,
|
|
27
|
+
Key: {
|
|
28
|
+
pk: partition,
|
|
29
|
+
sk: String(args.id)
|
|
30
|
+
},
|
|
31
|
+
ConsistentRead: true
|
|
32
|
+
}));
|
|
33
|
+
target = result.Item ? stripInternalKeys(result.Item) : null;
|
|
34
|
+
} else if (args.where) {
|
|
35
|
+
target = await findFirst(this, {
|
|
36
|
+
partition,
|
|
37
|
+
where: args.where
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!target) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const merged = {
|
|
44
|
+
...target,
|
|
45
|
+
...args.versionData,
|
|
46
|
+
id: target['id']
|
|
47
|
+
};
|
|
48
|
+
// Project both the version-row metadata layer and the inner `version`
|
|
49
|
+
// snapshot. Catches stray keys at either layer plus stale leaks already
|
|
50
|
+
// persisted on the target.
|
|
51
|
+
const projected = projectVersionRow(this, {
|
|
52
|
+
kind: 'collection',
|
|
53
|
+
slug: args.collection
|
|
54
|
+
}, merged);
|
|
55
|
+
await docClient.send(new PutCommand({
|
|
56
|
+
TableName: this.tableName,
|
|
57
|
+
Item: normalizeForDynamo({
|
|
58
|
+
...projected,
|
|
59
|
+
pk: partition,
|
|
60
|
+
sk: String(projected['id'])
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
if (args.returning === false) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return projected;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=updateVersion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/updateVersion.ts"],"sourcesContent":["import type { UpdateVersion } from 'payload'\n\nimport { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { findFirst } from './utilities/findFirst.js'\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { projectVersionRow } from './utilities/resolveSchema.js'\nimport { stripInternalKeys } from './utilities/stripInternalKeys.js'\n\n/**\n * Read-merge-write against the versions partition. `versionData` is\n * splatted onto the existing row, which means Payload-passed metadata\n * (`createdAt`, `updatedAt`, `latest`, `parent`, `publishedLocale`) and the\n * inner `version` payload all overlay correctly.\n *\n * The adapter does not cascade `latest=true` flips — Payload is responsible\n * for clearing the previous latest by issuing a separate `updateVersion`\n * call. This matches the `createVersion` boundary: `createVersion` owns the\n * cascade because it knows it's making a new latest; `updateVersion` writes\n * exactly what it's told.\n */\nexport const updateVersion: UpdateVersion = async function updateVersion(\n this: DynamoAdapter,\n args,\n) {\n const docClient = this.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const partition = this.resolveVersionsPartition(args.collection)\n\n let target: null | Record<string, unknown> = null\n\n if (args.id !== undefined && args.id !== null) {\n const result = await docClient.send(\n new GetCommand({\n TableName: this.tableName,\n Key: { pk: partition, sk: String(args.id) },\n ConsistentRead: true,\n }),\n )\n target = result.Item ? stripInternalKeys(result.Item) : null\n } else if (args.where) {\n target = await findFirst(this, { partition, where: args.where })\n }\n\n if (!target) {\n return null as never\n }\n\n const merged: Record<string, unknown> = {\n ...target,\n ...args.versionData,\n id: target['id'],\n }\n // Project both the version-row metadata layer and the inner `version`\n // snapshot. Catches stray keys at either layer plus stale leaks already\n // persisted on the target.\n const projected = projectVersionRow(\n this,\n { kind: 'collection', slug: args.collection },\n merged,\n )\n\n await docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: normalizeForDynamo({\n ...projected,\n pk: partition,\n sk: String(projected['id']),\n }),\n }),\n )\n\n if (args.returning === false) {\n return null as never\n }\n return projected as never\n}\n"],"names":["GetCommand","PutCommand","findFirst","normalizeForDynamo","projectVersionRow","stripInternalKeys","updateVersion","args","docClient","Error","partition","resolveVersionsPartition","collection","target","id","undefined","result","send","TableName","tableName","Key","pk","sk","String","ConsistentRead","Item","where","merged","versionData","projected","kind","slug","returning"],"mappings":"AAEA,SAASA,UAAU,EAAEC,UAAU,QAAQ,wBAAuB;AAI9D,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,iBAAiB,QAAQ,+BAA8B;AAChE,SAASC,iBAAiB,QAAQ,mCAAkC;AAEpE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,gBAA+B,eAAeA,cAEzDC,IAAI;IAEJ,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,YAAY,IAAI,CAACC,wBAAwB,CAACJ,KAAKK,UAAU;IAE/D,IAAIC,SAAyC;IAE7C,IAAIN,KAAKO,EAAE,KAAKC,aAAaR,KAAKO,EAAE,KAAK,MAAM;QAC7C,MAAME,SAAS,MAAMR,UAAUS,IAAI,CACjC,IAAIjB,WAAW;YACbkB,WAAW,IAAI,CAACC,SAAS;YACzBC,KAAK;gBAAEC,IAAIX;gBAAWY,IAAIC,OAAOhB,KAAKO,EAAE;YAAE;YAC1CU,gBAAgB;QAClB;QAEFX,SAASG,OAAOS,IAAI,GAAGpB,kBAAkBW,OAAOS,IAAI,IAAI;IAC1D,OAAO,IAAIlB,KAAKmB,KAAK,EAAE;QACrBb,SAAS,MAAMX,UAAU,IAAI,EAAE;YAAEQ;YAAWgB,OAAOnB,KAAKmB,KAAK;QAAC;IAChE;IAEA,IAAI,CAACb,QAAQ;QACX,OAAO;IACT;IAEA,MAAMc,SAAkC;QACtC,GAAGd,MAAM;QACT,GAAGN,KAAKqB,WAAW;QACnBd,IAAID,MAAM,CAAC,KAAK;IAClB;IACA,sEAAsE;IACtE,wEAAwE;IACxE,2BAA2B;IAC3B,MAAMgB,YAAYzB,kBAChB,IAAI,EACJ;QAAE0B,MAAM;QAAcC,MAAMxB,KAAKK,UAAU;IAAC,GAC5Ce;IAGF,MAAMnB,UAAUS,IAAI,CAClB,IAAIhB,WAAW;QACbiB,WAAW,IAAI,CAACC,SAAS;QACzBM,MAAMtB,mBAAmB;YACvB,GAAG0B,SAAS;YACZR,IAAIX;YACJY,IAAIC,OAAOM,SAAS,CAAC,KAAK;QAC5B;IACF;IAGF,IAAItB,KAAKyB,SAAS,KAAK,OAAO;QAC5B,OAAO;IACT;IACA,OAAOH;AACT,EAAC"}
|
package/dist/upsert.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Upsert } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Locate by `where`; merge-and-put if found, put-with-fresh-id otherwise.
|
|
4
|
+
*
|
|
5
|
+
* Inlined rather than dispatching to `create`/`updateOne` via `.call(this)` —
|
|
6
|
+
* piping through their `this`-typed signatures requires casts that obscure
|
|
7
|
+
* the small amount of logic this function actually owns.
|
|
8
|
+
*/
|
|
9
|
+
export declare const upsert: Upsert;
|
|
10
|
+
//# sourceMappingURL=upsert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upsert.d.ts","sourceRoot":"","sources":["../src/upsert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAWrC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,EAAE,MAgDpB,CAAA"}
|
package/dist/upsert.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { findFirst } from './utilities/findFirst.js';
|
|
4
|
+
import { normalizeForDynamo } from './utilities/normalizeForDynamo.js';
|
|
5
|
+
import { projectForCollection } from './utilities/resolveSchema.js';
|
|
6
|
+
/**
|
|
7
|
+
* Locate by `where`; merge-and-put if found, put-with-fresh-id otherwise.
|
|
8
|
+
*
|
|
9
|
+
* Inlined rather than dispatching to `create`/`updateOne` via `.call(this)` —
|
|
10
|
+
* piping through their `this`-typed signatures requires casts that obscure
|
|
11
|
+
* the small amount of logic this function actually owns.
|
|
12
|
+
*/ export const upsert = async function upsert({ collection, data, returning, where }) {
|
|
13
|
+
const docClient = this.docClient;
|
|
14
|
+
if (!docClient) {
|
|
15
|
+
throw new Error('payload-ddb: docClient is not initialized — call connect() first.');
|
|
16
|
+
}
|
|
17
|
+
const partition = this.resolvePartition(collection);
|
|
18
|
+
const found = await findFirst(this, {
|
|
19
|
+
partition,
|
|
20
|
+
where
|
|
21
|
+
});
|
|
22
|
+
const now = new Date().toISOString();
|
|
23
|
+
let item;
|
|
24
|
+
if (found) {
|
|
25
|
+
// Bump `updatedAt` automatically; explicit value in `data` still wins.
|
|
26
|
+
item = {
|
|
27
|
+
...found,
|
|
28
|
+
...data,
|
|
29
|
+
id: found['id'],
|
|
30
|
+
updatedAt: data['updatedAt'] ?? now
|
|
31
|
+
};
|
|
32
|
+
} else {
|
|
33
|
+
const id = data['id'] ?? randomUUID();
|
|
34
|
+
// Same nullish-coalesce pattern as `create`: explicit values in `data`
|
|
35
|
+
// win, but a payload-passed `undefined` doesn't wipe the default.
|
|
36
|
+
item = {
|
|
37
|
+
...data,
|
|
38
|
+
id,
|
|
39
|
+
createdAt: data['createdAt'] ?? now,
|
|
40
|
+
updatedAt: data['updatedAt'] ?? now
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const projected = projectForCollection(this, collection, item);
|
|
44
|
+
await docClient.send(new PutCommand({
|
|
45
|
+
TableName: this.tableName,
|
|
46
|
+
Item: normalizeForDynamo({
|
|
47
|
+
...projected,
|
|
48
|
+
pk: partition,
|
|
49
|
+
sk: String(projected['id'])
|
|
50
|
+
})
|
|
51
|
+
}));
|
|
52
|
+
return returning === false ? null : projected;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=upsert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/upsert.ts"],"sourcesContent":["import type { Upsert } from 'payload'\n\nimport { PutCommand } from '@aws-sdk/lib-dynamodb'\nimport { randomUUID } from 'node:crypto'\n\nimport type { DynamoAdapter } from './types.js'\n\nimport { findFirst } from './utilities/findFirst.js'\nimport { normalizeForDynamo } from './utilities/normalizeForDynamo.js'\nimport { projectForCollection } from './utilities/resolveSchema.js'\n\n/**\n * Locate by `where`; merge-and-put if found, put-with-fresh-id otherwise.\n *\n * Inlined rather than dispatching to `create`/`updateOne` via `.call(this)` —\n * piping through their `this`-typed signatures requires casts that obscure\n * the small amount of logic this function actually owns.\n */\nexport const upsert: Upsert = async function upsert(\n this: DynamoAdapter,\n { collection, data, returning, where },\n) {\n const docClient = this.docClient\n if (!docClient) {\n throw new Error('payload-ddb: docClient is not initialized — call connect() first.')\n }\n\n const partition = this.resolvePartition(collection)\n const found = await findFirst(this, { partition, where })\n\n const now = new Date().toISOString()\n let item: Record<string, unknown>\n if (found) {\n // Bump `updatedAt` automatically; explicit value in `data` still wins.\n item = {\n ...found,\n ...data,\n id: found['id'],\n updatedAt: data['updatedAt'] ?? now,\n }\n } else {\n const id = data['id'] ?? randomUUID()\n // Same nullish-coalesce pattern as `create`: explicit values in `data`\n // win, but a payload-passed `undefined` doesn't wipe the default.\n item = {\n ...data,\n id,\n createdAt: data['createdAt'] ?? now,\n updatedAt: data['updatedAt'] ?? now,\n }\n }\n\n const projected = projectForCollection(this, collection, item)\n\n await docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: normalizeForDynamo({\n ...projected,\n pk: partition,\n sk: String(projected['id']),\n }),\n }),\n )\n\n return returning === false ? (null as never) : (projected as never)\n}\n"],"names":["PutCommand","randomUUID","findFirst","normalizeForDynamo","projectForCollection","upsert","collection","data","returning","where","docClient","Error","partition","resolvePartition","found","now","Date","toISOString","item","id","updatedAt","createdAt","projected","send","TableName","tableName","Item","pk","sk","String"],"mappings":"AAEA,SAASA,UAAU,QAAQ,wBAAuB;AAClD,SAASC,UAAU,QAAQ,cAAa;AAIxC,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,oBAAoB,QAAQ,+BAA8B;AAEnE;;;;;;CAMC,GACD,OAAO,MAAMC,SAAiB,eAAeA,OAE3C,EAAEC,UAAU,EAAEC,IAAI,EAAEC,SAAS,EAAEC,KAAK,EAAE;IAEtC,MAAMC,YAAY,IAAI,CAACA,SAAS;IAChC,IAAI,CAACA,WAAW;QACd,MAAM,IAAIC,MAAM;IAClB;IAEA,MAAMC,YAAY,IAAI,CAACC,gBAAgB,CAACP;IACxC,MAAMQ,QAAQ,MAAMZ,UAAU,IAAI,EAAE;QAAEU;QAAWH;IAAM;IAEvD,MAAMM,MAAM,IAAIC,OAAOC,WAAW;IAClC,IAAIC;IACJ,IAAIJ,OAAO;QACT,uEAAuE;QACvEI,OAAO;YACL,GAAGJ,KAAK;YACR,GAAGP,IAAI;YACPY,IAAIL,KAAK,CAAC,KAAK;YACfM,WAAWb,IAAI,CAAC,YAAY,IAAIQ;QAClC;IACF,OAAO;QACL,MAAMI,KAAKZ,IAAI,CAAC,KAAK,IAAIN;QACzB,uEAAuE;QACvE,kEAAkE;QAClEiB,OAAO;YACL,GAAGX,IAAI;YACPY;YACAE,WAAWd,IAAI,CAAC,YAAY,IAAIQ;YAChCK,WAAWb,IAAI,CAAC,YAAY,IAAIQ;QAClC;IACF;IAEA,MAAMO,YAAYlB,qBAAqB,IAAI,EAAEE,YAAYY;IAEzD,MAAMR,UAAUa,IAAI,CAClB,IAAIvB,WAAW;QACbwB,WAAW,IAAI,CAACC,SAAS;QACzBC,MAAMvB,mBAAmB;YACvB,GAAGmB,SAAS;YACZK,IAAIf;YACJgB,IAAIC,OAAOP,SAAS,CAAC,KAAK;QAC5B;IACF;IAGF,OAAOd,cAAc,QAAS,OAAkBc;AAClD,EAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Sort } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Sort an array in place according to Payload's sort convention:
|
|
4
|
+
* `'field'` ascending, `'-field'` descending. Multiple sort keys are applied
|
|
5
|
+
* as tiebreakers in order.
|
|
6
|
+
*
|
|
7
|
+
* Sorts in JS because DynamoDB `Scan` returns rows in indeterminate order;
|
|
8
|
+
* Query-on-GSI sort comes when we wire access patterns up later.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applySorts(items: Record<string, unknown>[], sort: Sort | undefined): void;
|
|
11
|
+
//# sourceMappingURL=applySorts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applySorts.d.ts","sourceRoot":"","sources":["../../src/utilities/applySorts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAInC;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,IAAI,CAmBzF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getByPath } from './getByPath.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sort an array in place according to Payload's sort convention:
|
|
4
|
+
* `'field'` ascending, `'-field'` descending. Multiple sort keys are applied
|
|
5
|
+
* as tiebreakers in order.
|
|
6
|
+
*
|
|
7
|
+
* Sorts in JS because DynamoDB `Scan` returns rows in indeterminate order;
|
|
8
|
+
* Query-on-GSI sort comes when we wire access patterns up later.
|
|
9
|
+
*/ export function applySorts(items, sort) {
|
|
10
|
+
if (!sort) return;
|
|
11
|
+
const keys = Array.isArray(sort) ? sort : [
|
|
12
|
+
sort
|
|
13
|
+
];
|
|
14
|
+
if (keys.length === 0) return;
|
|
15
|
+
items.sort((a, b)=>{
|
|
16
|
+
for (const raw of keys){
|
|
17
|
+
if (!raw) continue;
|
|
18
|
+
const descending = raw.startsWith('-');
|
|
19
|
+
const path = descending ? raw.slice(1) : raw;
|
|
20
|
+
const av = getByPath(a, path);
|
|
21
|
+
const bv = getByPath(b, path);
|
|
22
|
+
const cmp = compare(av, bv);
|
|
23
|
+
if (cmp !== 0) {
|
|
24
|
+
return descending ? -cmp : cmp;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function compare(a, b) {
|
|
31
|
+
if (a === b) return 0;
|
|
32
|
+
// Nullish values sort before defined values, ascending. Matches Mongo's
|
|
33
|
+
// default behavior and avoids undefined-vs-string comparison NaNs.
|
|
34
|
+
const aMissing = a === undefined || a === null;
|
|
35
|
+
const bMissing = b === undefined || b === null;
|
|
36
|
+
if (aMissing && bMissing) return 0;
|
|
37
|
+
if (aMissing) return -1;
|
|
38
|
+
if (bMissing) return 1;
|
|
39
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
40
|
+
// Fall back to lexicographic string compare for everything else (dates as
|
|
41
|
+
// ISO strings, booleans, etc.).
|
|
42
|
+
const as = String(a);
|
|
43
|
+
const bs = String(b);
|
|
44
|
+
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=applySorts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/applySorts.ts"],"sourcesContent":["import type { Sort } from 'payload'\n\nimport { getByPath } from './getByPath.js'\n\n/**\n * Sort an array in place according to Payload's sort convention:\n * `'field'` ascending, `'-field'` descending. Multiple sort keys are applied\n * as tiebreakers in order.\n *\n * Sorts in JS because DynamoDB `Scan` returns rows in indeterminate order;\n * Query-on-GSI sort comes when we wire access patterns up later.\n */\nexport function applySorts(items: Record<string, unknown>[], sort: Sort | undefined): void {\n if (!sort) return\n const keys = Array.isArray(sort) ? sort : [sort]\n if (keys.length === 0) return\n\n items.sort((a, b) => {\n for (const raw of keys) {\n if (!raw) continue\n const descending = raw.startsWith('-')\n const path = descending ? raw.slice(1) : raw\n const av = getByPath(a, path)\n const bv = getByPath(b, path)\n const cmp = compare(av, bv)\n if (cmp !== 0) {\n return descending ? -cmp : cmp\n }\n }\n return 0\n })\n}\n\nfunction compare(a: unknown, b: unknown): number {\n if (a === b) return 0\n // Nullish values sort before defined values, ascending. Matches Mongo's\n // default behavior and avoids undefined-vs-string comparison NaNs.\n const aMissing = a === undefined || a === null\n const bMissing = b === undefined || b === null\n if (aMissing && bMissing) return 0\n if (aMissing) return -1\n if (bMissing) return 1\n if (typeof a === 'number' && typeof b === 'number') return a - b\n // Fall back to lexicographic string compare for everything else (dates as\n // ISO strings, booleans, etc.).\n const as = String(a)\n const bs = String(b)\n return as < bs ? -1 : as > bs ? 1 : 0\n}\n"],"names":["getByPath","applySorts","items","sort","keys","Array","isArray","length","a","b","raw","descending","startsWith","path","slice","av","bv","cmp","compare","aMissing","undefined","bMissing","as","String","bs"],"mappings":"AAEA,SAASA,SAAS,QAAQ,iBAAgB;AAE1C;;;;;;;CAOC,GACD,OAAO,SAASC,WAAWC,KAAgC,EAAEC,IAAsB;IACjF,IAAI,CAACA,MAAM;IACX,MAAMC,OAAOC,MAAMC,OAAO,CAACH,QAAQA,OAAO;QAACA;KAAK;IAChD,IAAIC,KAAKG,MAAM,KAAK,GAAG;IAEvBL,MAAMC,IAAI,CAAC,CAACK,GAAGC;QACb,KAAK,MAAMC,OAAON,KAAM;YACtB,IAAI,CAACM,KAAK;YACV,MAAMC,aAAaD,IAAIE,UAAU,CAAC;YAClC,MAAMC,OAAOF,aAAaD,IAAII,KAAK,CAAC,KAAKJ;YACzC,MAAMK,KAAKf,UAAUQ,GAAGK;YACxB,MAAMG,KAAKhB,UAAUS,GAAGI;YACxB,MAAMI,MAAMC,QAAQH,IAAIC;YACxB,IAAIC,QAAQ,GAAG;gBACb,OAAON,aAAa,CAACM,MAAMA;YAC7B;QACF;QACA,OAAO;IACT;AACF;AAEA,SAASC,QAAQV,CAAU,EAAEC,CAAU;IACrC,IAAID,MAAMC,GAAG,OAAO;IACpB,wEAAwE;IACxE,mEAAmE;IACnE,MAAMU,WAAWX,MAAMY,aAAaZ,MAAM;IAC1C,MAAMa,WAAWZ,MAAMW,aAAaX,MAAM;IAC1C,IAAIU,YAAYE,UAAU,OAAO;IACjC,IAAIF,UAAU,OAAO,CAAC;IACtB,IAAIE,UAAU,OAAO;IACrB,IAAI,OAAOb,MAAM,YAAY,OAAOC,MAAM,UAAU,OAAOD,IAAIC;IAC/D,0EAA0E;IAC1E,gCAAgC;IAChC,MAAMa,KAAKC,OAAOf;IAClB,MAAMgB,KAAKD,OAAOd;IAClB,OAAOa,KAAKE,KAAK,CAAC,IAAIF,KAAKE,KAAK,IAAI;AACtC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Where } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Compile a Payload `Where` into a DynamoDB `FilterExpression` plus its
|
|
4
|
+
* companion `ExpressionAttributeNames` / `ExpressionAttributeValues` maps.
|
|
5
|
+
*
|
|
6
|
+
* Return contract:
|
|
7
|
+
* - `undefined` — no filter to apply (empty/missing `where`, or only no-op
|
|
8
|
+
* clauses like `and: []`). Callers should query without a filter.
|
|
9
|
+
* - `null` — the predicate is structurally always-false (e.g. an empty
|
|
10
|
+
* `id: { in: [] }`). Callers should short-circuit and return zero rows
|
|
11
|
+
* without ever hitting DynamoDB. Payload uses empty `in` as a "match
|
|
12
|
+
* nothing" sentinel (typically from access control resolving to no IDs);
|
|
13
|
+
* surfacing it as a filter would mean throwing or constructing an
|
|
14
|
+
* invalid `IN ()` clause.
|
|
15
|
+
* - `FilterExpression` — actual predicate to push down.
|
|
16
|
+
*
|
|
17
|
+
* Throws on any operator outside `operators.ts`'s supported set, identical
|
|
18
|
+
* to `matchesWhere` — coverage is enforced from one source so the in-JS
|
|
19
|
+
* path and the pushdown path can never disagree.
|
|
20
|
+
*
|
|
21
|
+
* Implementation notes:
|
|
22
|
+
* - Every field path segment becomes an `#nN` placeholder, sidestepping
|
|
23
|
+
* DynamoDB's reserved-word list entirely. Segments are deduped per-call so
|
|
24
|
+
* repeated fields produce one `#name` entry rather than many.
|
|
25
|
+
* - Every value becomes a `:vN` placeholder. We don't dedupe values because
|
|
26
|
+
* array members and repeated literals are cheap and the placeholder count
|
|
27
|
+
* keeps the expression readable.
|
|
28
|
+
* - Dotted paths in `where` keys are split on `.` so DynamoDB document-path
|
|
29
|
+
* syntax (`#a.#b`) is produced for nested fields.
|
|
30
|
+
* - `in: []` propagates a NEVER sentinel through the AND/OR algebra:
|
|
31
|
+
* AND with NEVER → NEVER (whole result `null`); OR with NEVER drops the
|
|
32
|
+
* NEVER branch; an OR whose every branch is NEVER itself becomes NEVER.
|
|
33
|
+
* - `not_in: []` is treated as no constraint (the complement of an empty
|
|
34
|
+
* set is everything, so omitting it from the expression matches semantics).
|
|
35
|
+
*/
|
|
36
|
+
export type FilterExpression = {
|
|
37
|
+
expression: string;
|
|
38
|
+
names: Record<string, string>;
|
|
39
|
+
values: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
export declare function buildFilterExpression(where: undefined | Where): FilterExpression | null | undefined;
|
|
42
|
+
//# sourceMappingURL=buildFilterExpression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildFilterExpression.d.ts","sourceRoot":"","sources":["../../src/utilities/buildFilterExpression.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAKpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC,CAAA;AAaD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,GAAG,KAAK,GACvB,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAoBrC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { normalizeForDynamo } from './normalizeForDynamo.js';
|
|
2
|
+
import { SUPPORTED_OPERATORS, unsupportedOperatorError } from './operators.js';
|
|
3
|
+
const NEVER = Symbol('payload-ddb-filter-never');
|
|
4
|
+
export function buildFilterExpression(where) {
|
|
5
|
+
if (!where) return undefined;
|
|
6
|
+
const ctx = {
|
|
7
|
+
nameByPlaceholder: {},
|
|
8
|
+
nameCounter: 0,
|
|
9
|
+
placeholderBySegment: new Map(),
|
|
10
|
+
valueCounter: 0,
|
|
11
|
+
values: {}
|
|
12
|
+
};
|
|
13
|
+
const expression = compile(where, ctx);
|
|
14
|
+
if (expression === NEVER) return null;
|
|
15
|
+
if (!expression) return undefined;
|
|
16
|
+
return {
|
|
17
|
+
expression,
|
|
18
|
+
names: ctx.nameByPlaceholder,
|
|
19
|
+
values: ctx.values
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function compile(where, ctx) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const [key, raw] of Object.entries(where)){
|
|
25
|
+
if (key === 'and') {
|
|
26
|
+
const inner = compileGroup(raw, ctx, 'AND');
|
|
27
|
+
if (inner === NEVER) return NEVER;
|
|
28
|
+
if (inner) parts.push(inner);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (key === 'or') {
|
|
32
|
+
const inner = compileGroup(raw, ctx, 'OR');
|
|
33
|
+
if (inner === NEVER) return NEVER;
|
|
34
|
+
if (inner) parts.push(inner);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
38
|
+
const path = pathRef(ctx, key);
|
|
39
|
+
const operators = raw;
|
|
40
|
+
for (const [operator, expected] of Object.entries(operators)){
|
|
41
|
+
if (!SUPPORTED_OPERATORS.has(operator)) {
|
|
42
|
+
throw unsupportedOperatorError(operator, key);
|
|
43
|
+
}
|
|
44
|
+
const expr = operatorToExpression(operator, path, expected, ctx);
|
|
45
|
+
if (expr === NEVER) return NEVER;
|
|
46
|
+
if (expr) parts.push(expr);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (parts.length === 0) return '';
|
|
50
|
+
if (parts.length === 1) return parts[0];
|
|
51
|
+
return parts.join(' AND ');
|
|
52
|
+
}
|
|
53
|
+
function compileGroup(raw, ctx, joiner) {
|
|
54
|
+
if (!Array.isArray(raw)) return '';
|
|
55
|
+
const subs = [];
|
|
56
|
+
let neverCount = 0;
|
|
57
|
+
let consideredCount = 0;
|
|
58
|
+
for (const sub of raw){
|
|
59
|
+
if (!sub || typeof sub !== 'object') continue;
|
|
60
|
+
consideredCount++;
|
|
61
|
+
const expr = compile(sub, ctx);
|
|
62
|
+
if (expr === NEVER) {
|
|
63
|
+
if (joiner === 'AND') return NEVER;
|
|
64
|
+
neverCount++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (expr) subs.push(expr);
|
|
68
|
+
}
|
|
69
|
+
// OR over branches that all collapsed to NEVER is NEVER itself.
|
|
70
|
+
if (joiner === 'OR' && consideredCount > 0 && neverCount === consideredCount) {
|
|
71
|
+
return NEVER;
|
|
72
|
+
}
|
|
73
|
+
if (subs.length === 0) return '';
|
|
74
|
+
if (subs.length === 1) return subs[0];
|
|
75
|
+
return `(${subs.join(` ${joiner} `)})`;
|
|
76
|
+
}
|
|
77
|
+
function pathRef(ctx, path) {
|
|
78
|
+
return path.split('.').map((segment)=>segmentRef(ctx, segment)).join('.');
|
|
79
|
+
}
|
|
80
|
+
function segmentRef(ctx, segment) {
|
|
81
|
+
const cached = ctx.placeholderBySegment.get(segment);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
const placeholder = `#n${ctx.nameCounter++}`;
|
|
84
|
+
ctx.nameByPlaceholder[placeholder] = segment;
|
|
85
|
+
ctx.placeholderBySegment.set(segment, placeholder);
|
|
86
|
+
return placeholder;
|
|
87
|
+
}
|
|
88
|
+
function valueRef(ctx, value) {
|
|
89
|
+
const placeholder = `:v${ctx.valueCounter++}`;
|
|
90
|
+
// Normalize so Date literals in `where` compare against the ISO strings we
|
|
91
|
+
// actually stored. Without this, `{ updatedAt: { greater_than: new Date() } }`
|
|
92
|
+
// would push an empty map literal into the filter and DynamoDB would reject
|
|
93
|
+
// the comparison.
|
|
94
|
+
ctx.values[placeholder] = normalizeForDynamo(value);
|
|
95
|
+
return placeholder;
|
|
96
|
+
}
|
|
97
|
+
function operatorToExpression(operator, path, expected, ctx) {
|
|
98
|
+
switch(operator){
|
|
99
|
+
case 'equals':
|
|
100
|
+
return `${path} = ${valueRef(ctx, expected)}`;
|
|
101
|
+
case 'not_equals':
|
|
102
|
+
return `${path} <> ${valueRef(ctx, expected)}`;
|
|
103
|
+
case 'greater_than':
|
|
104
|
+
return `${path} > ${valueRef(ctx, expected)}`;
|
|
105
|
+
case 'greater_than_equal':
|
|
106
|
+
return `${path} >= ${valueRef(ctx, expected)}`;
|
|
107
|
+
case 'less_than':
|
|
108
|
+
return `${path} < ${valueRef(ctx, expected)}`;
|
|
109
|
+
case 'less_than_equal':
|
|
110
|
+
return `${path} <= ${valueRef(ctx, expected)}`;
|
|
111
|
+
case 'exists':
|
|
112
|
+
return expected ? `attribute_exists(${path})` : `attribute_not_exists(${path})`;
|
|
113
|
+
case 'like':
|
|
114
|
+
// JS-only: `whereHasJsOnlyOperator` should have steered the helpers
|
|
115
|
+
// away from compiling this `where` server-side. Returning '' is a
|
|
116
|
+
// belt-and-braces no-op in case a caller compiles directly.
|
|
117
|
+
return '';
|
|
118
|
+
case 'in':
|
|
119
|
+
{
|
|
120
|
+
if (!Array.isArray(expected)) return '';
|
|
121
|
+
if (expected.length === 0) return NEVER;
|
|
122
|
+
const refs = expected.map((v)=>valueRef(ctx, v));
|
|
123
|
+
return `${path} IN (${refs.join(', ')})`;
|
|
124
|
+
}
|
|
125
|
+
case 'not_in':
|
|
126
|
+
{
|
|
127
|
+
if (!Array.isArray(expected) || expected.length === 0) return '';
|
|
128
|
+
const refs = expected.map((v)=>valueRef(ctx, v));
|
|
129
|
+
return `NOT (${path} IN (${refs.join(', ')}))`;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
// Unreachable — gated by SUPPORTED_OPERATORS check above. Belt-and-braces.
|
|
133
|
+
throw unsupportedOperatorError(operator, path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=buildFilterExpression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/buildFilterExpression.ts"],"sourcesContent":["import type { Where } from 'payload'\n\nimport { normalizeForDynamo } from './normalizeForDynamo.js'\nimport { SUPPORTED_OPERATORS, unsupportedOperatorError } from './operators.js'\n\n/**\n * Compile a Payload `Where` into a DynamoDB `FilterExpression` plus its\n * companion `ExpressionAttributeNames` / `ExpressionAttributeValues` maps.\n *\n * Return contract:\n * - `undefined` — no filter to apply (empty/missing `where`, or only no-op\n * clauses like `and: []`). Callers should query without a filter.\n * - `null` — the predicate is structurally always-false (e.g. an empty\n * `id: { in: [] }`). Callers should short-circuit and return zero rows\n * without ever hitting DynamoDB. Payload uses empty `in` as a \"match\n * nothing\" sentinel (typically from access control resolving to no IDs);\n * surfacing it as a filter would mean throwing or constructing an\n * invalid `IN ()` clause.\n * - `FilterExpression` — actual predicate to push down.\n *\n * Throws on any operator outside `operators.ts`'s supported set, identical\n * to `matchesWhere` — coverage is enforced from one source so the in-JS\n * path and the pushdown path can never disagree.\n *\n * Implementation notes:\n * - Every field path segment becomes an `#nN` placeholder, sidestepping\n * DynamoDB's reserved-word list entirely. Segments are deduped per-call so\n * repeated fields produce one `#name` entry rather than many.\n * - Every value becomes a `:vN` placeholder. We don't dedupe values because\n * array members and repeated literals are cheap and the placeholder count\n * keeps the expression readable.\n * - Dotted paths in `where` keys are split on `.` so DynamoDB document-path\n * syntax (`#a.#b`) is produced for nested fields.\n * - `in: []` propagates a NEVER sentinel through the AND/OR algebra:\n * AND with NEVER → NEVER (whole result `null`); OR with NEVER drops the\n * NEVER branch; an OR whose every branch is NEVER itself becomes NEVER.\n * - `not_in: []` is treated as no constraint (the complement of an empty\n * set is everything, so omitting it from the expression matches semantics).\n */\nexport type FilterExpression = {\n expression: string\n names: Record<string, string>\n values: Record<string, unknown>\n}\n\nconst NEVER = Symbol('payload-ddb-filter-never')\ntype Compiled = string | typeof NEVER\n\ntype Ctx = {\n nameByPlaceholder: Record<string, string>\n nameCounter: number\n placeholderBySegment: Map<string, string>\n valueCounter: number\n values: Record<string, unknown>\n}\n\nexport function buildFilterExpression(\n where: undefined | Where,\n): FilterExpression | null | undefined {\n if (!where) return undefined\n\n const ctx: Ctx = {\n nameByPlaceholder: {},\n nameCounter: 0,\n placeholderBySegment: new Map(),\n valueCounter: 0,\n values: {},\n }\n\n const expression = compile(where, ctx)\n if (expression === NEVER) return null\n if (!expression) return undefined\n\n return {\n expression,\n names: ctx.nameByPlaceholder,\n values: ctx.values,\n }\n}\n\nfunction compile(where: Where, ctx: Ctx): Compiled {\n const parts: string[] = []\n\n for (const [key, raw] of Object.entries(where)) {\n if (key === 'and') {\n const inner = compileGroup(raw, ctx, 'AND')\n if (inner === NEVER) return NEVER\n if (inner) parts.push(inner)\n continue\n }\n if (key === 'or') {\n const inner = compileGroup(raw, ctx, 'OR')\n if (inner === NEVER) return NEVER\n if (inner) parts.push(inner)\n continue\n }\n\n if (!raw || typeof raw !== 'object') continue\n\n const path = pathRef(ctx, key)\n const operators = raw as Record<string, unknown>\n\n for (const [operator, expected] of Object.entries(operators)) {\n if (!SUPPORTED_OPERATORS.has(operator)) {\n throw unsupportedOperatorError(operator, key)\n }\n const expr = operatorToExpression(operator, path, expected, ctx)\n if (expr === NEVER) return NEVER\n if (expr) parts.push(expr)\n }\n }\n\n if (parts.length === 0) return ''\n if (parts.length === 1) return parts[0]!\n return parts.join(' AND ')\n}\n\nfunction compileGroup(raw: unknown, ctx: Ctx, joiner: 'AND' | 'OR'): Compiled {\n if (!Array.isArray(raw)) return ''\n const subs: string[] = []\n let neverCount = 0\n let consideredCount = 0\n for (const sub of raw) {\n if (!sub || typeof sub !== 'object') continue\n consideredCount++\n const expr = compile(sub as Where, ctx)\n if (expr === NEVER) {\n if (joiner === 'AND') return NEVER\n neverCount++\n continue\n }\n if (expr) subs.push(expr)\n }\n // OR over branches that all collapsed to NEVER is NEVER itself.\n if (joiner === 'OR' && consideredCount > 0 && neverCount === consideredCount) {\n return NEVER\n }\n if (subs.length === 0) return ''\n if (subs.length === 1) return subs[0]!\n return `(${subs.join(` ${joiner} `)})`\n}\n\nfunction pathRef(ctx: Ctx, path: string): string {\n return path\n .split('.')\n .map((segment) => segmentRef(ctx, segment))\n .join('.')\n}\n\nfunction segmentRef(ctx: Ctx, segment: string): string {\n const cached = ctx.placeholderBySegment.get(segment)\n if (cached) return cached\n const placeholder = `#n${ctx.nameCounter++}`\n ctx.nameByPlaceholder[placeholder] = segment\n ctx.placeholderBySegment.set(segment, placeholder)\n return placeholder\n}\n\nfunction valueRef(ctx: Ctx, value: unknown): string {\n const placeholder = `:v${ctx.valueCounter++}`\n // Normalize so Date literals in `where` compare against the ISO strings we\n // actually stored. Without this, `{ updatedAt: { greater_than: new Date() } }`\n // would push an empty map literal into the filter and DynamoDB would reject\n // the comparison.\n ctx.values[placeholder] = normalizeForDynamo(value)\n return placeholder\n}\n\nfunction operatorToExpression(\n operator: string,\n path: string,\n expected: unknown,\n ctx: Ctx,\n): Compiled {\n switch (operator) {\n case 'equals':\n return `${path} = ${valueRef(ctx, expected)}`\n case 'not_equals':\n return `${path} <> ${valueRef(ctx, expected)}`\n case 'greater_than':\n return `${path} > ${valueRef(ctx, expected)}`\n case 'greater_than_equal':\n return `${path} >= ${valueRef(ctx, expected)}`\n case 'less_than':\n return `${path} < ${valueRef(ctx, expected)}`\n case 'less_than_equal':\n return `${path} <= ${valueRef(ctx, expected)}`\n case 'exists':\n return expected ? `attribute_exists(${path})` : `attribute_not_exists(${path})`\n case 'like':\n // JS-only: `whereHasJsOnlyOperator` should have steered the helpers\n // away from compiling this `where` server-side. Returning '' is a\n // belt-and-braces no-op in case a caller compiles directly.\n return ''\n case 'in': {\n if (!Array.isArray(expected)) return ''\n if (expected.length === 0) return NEVER\n const refs = expected.map((v) => valueRef(ctx, v))\n return `${path} IN (${refs.join(', ')})`\n }\n case 'not_in': {\n if (!Array.isArray(expected) || expected.length === 0) return ''\n const refs = expected.map((v) => valueRef(ctx, v))\n return `NOT (${path} IN (${refs.join(', ')}))`\n }\n default:\n // Unreachable — gated by SUPPORTED_OPERATORS check above. Belt-and-braces.\n throw unsupportedOperatorError(operator, path)\n }\n}\n"],"names":["normalizeForDynamo","SUPPORTED_OPERATORS","unsupportedOperatorError","NEVER","Symbol","buildFilterExpression","where","undefined","ctx","nameByPlaceholder","nameCounter","placeholderBySegment","Map","valueCounter","values","expression","compile","names","parts","key","raw","Object","entries","inner","compileGroup","push","path","pathRef","operators","operator","expected","has","expr","operatorToExpression","length","join","joiner","Array","isArray","subs","neverCount","consideredCount","sub","split","map","segment","segmentRef","cached","get","placeholder","set","valueRef","value","refs","v"],"mappings":"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"}
|