@exabugs/dynamodb-client 1.6.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/scripts/operations/bulkRecordRepair.js +2 -1
  3. package/dist/scripts/operations/bulkRecordRepair.js.map +1 -1
  4. package/dist/server/handler.cjs +174 -76
  5. package/dist/server/handler.cjs.map +4 -4
  6. package/dist/server/media-handler.cjs +2 -2
  7. package/dist/server/media-process-handler-dlq.cjs +179 -81
  8. package/dist/server/media-process-handler-dlq.cjs.map +4 -4
  9. package/dist/server/media-process-handler.cjs +179 -81
  10. package/dist/server/media-process-handler.cjs.map +4 -4
  11. package/dist/server/operations/find/idQuery.d.ts.map +1 -1
  12. package/dist/server/operations/find/idQuery.js +57 -39
  13. package/dist/server/operations/find/idQuery.js.map +1 -1
  14. package/dist/server/operations/find/utils.js +2 -2
  15. package/dist/server/operations/find/utils.js.map +1 -1
  16. package/dist/server/operations/updateMany.d.ts.map +1 -1
  17. package/dist/server/operations/updateMany.js +6 -76
  18. package/dist/server/operations/updateMany.js.map +1 -1
  19. package/dist/server/operations/updateOne.d.ts.map +1 -1
  20. package/dist/server/operations/updateOne.js +9 -1
  21. package/dist/server/operations/updateOne.js.map +1 -1
  22. package/dist/server/utils/mergePatch.d.ts +8 -0
  23. package/dist/server/utils/mergePatch.d.ts.map +1 -0
  24. package/dist/server/utils/mergePatch.js +127 -0
  25. package/dist/server/utils/mergePatch.js.map +1 -0
  26. package/dist/server/utils/timestamps.d.ts.map +1 -1
  27. package/dist/server/utils/timestamps.js +2 -1
  28. package/dist/server/utils/timestamps.js.map +1 -1
  29. package/dist/sharp-layer.zip +0 -0
  30. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.8.0] - 2026-10-12
11
+
12
+ ### Fixed
13
+
14
+ - `sort: id` にフィルタを付けた一覧が、ページごとに全レコードを読み直していた(全ページで O(ページ数 × 件数)、更新中は取りこぼしの原因)。ID 順のカーソルで読み進める方式に変更し、全ページを通した読み取りは O(件数)、走査中の更新でも取りこぼし・重複が出ない (#50)
15
+ - `bulkRecordRepair` が既定ソート(updatedAt)で列挙していたため、修復中の更新で取りこぼし・重複が出うる問題を、id 順の列挙に変更して解消 (#51)
16
+
17
+ ### Changed
18
+
19
+ - `sort: id` + フィルタの `nextToken` は、オフセットではなく最後に読んだレコードのキーになった。デプロイをまたいで途中のページネーションを続けると、古いトークンは 400 になる
20
+ - `sort: id` + フィルタは、ページが埋まる(perPage 件)か末尾に達するまで読んでから返す。一致が疎だと 1 リクエストで多くのレコードを読むので、頻繁に使う条件はシャドー(インデックス)の対象フィールドで絞ること。`perPage` 未満なら最終ページ、という前提は従来どおり保たれる
21
+
22
+ ## [1.7.0] - 2026-10-12
23
+
24
+ ### Fixed
25
+
26
+ - 更新のドット記法パス(例: `sections.1.t`)が配列をオブジェクトに壊していた。配列は要素単位で更新し、範囲外・非数値の添字は 400 で拒否する。`__proto__` を含むパスも拒否する (#48)
27
+ - 更新時に `updatedAt` が変わらなかった(既存の値が優先されていた)。パッチ側に付与して毎回更新する。クライアントが値を指定した場合はそれを尊重し、`null` / `undefined` は未指定として扱う (#48)
28
+ - 解釈できないフィルタ指定が 500 (`CONFIG_ERROR`) になっていた。400 (`INVALID_FILTER`) を返す (#48)
29
+
30
+ ### Changed
31
+
32
+ - **`updatedAt` を明示しない更新で、値が毎回変わる**ようになった。`updatedAt` 順の一覧は更新のたびに並びが変わり、`updatedAt` のシャドーの再作成により 1 レコードあたりの書き込み項目が増える
33
+ - ドット記法で、存在しない(またはオブジェクト以外の)親に対する `null`(削除)は何もしなくなった(従来は `{a: null}` のような残骸が残っていた)
34
+
10
35
  ## [1.6.2] - 2026-10-11
11
36
 
12
37
  ### Security
@@ -32,7 +32,8 @@ export async function repairAllRecords(collection, dryRun) {
32
32
  * @returns 全レコードの配列
33
33
  */
34
34
  async function fetchAllRecords(collection) {
35
- return await collection.find({}).toArrayAll();
35
+ // 修復中にレコードが更新されても取りこぼし・重複が出ないよう、不変の id 順で列挙する
36
+ return await collection.find({}).sort({ id: 'asc' }).toArrayAll();
36
37
  }
37
38
  /**
38
39
  * 単一レコードを処理
@@ -1 +1 @@
1
- {"version":3,"file":"bulkRecordRepair.js","sourceRoot":"","sources":["../../../src/scripts/operations/bulkRecordRepair.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,oBAAoB,EACpB,oBAAoB,EACpB,2BAA2B,EAC3B,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAA4E,EAC5E,MAAe;IAEf,MAAM,KAAK,GAAgB;QACzB,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,CAAC;QACd,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,EAAE;KACX,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtD,WAAW;IACX,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,SAAS,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC;IAE/C,WAAW;IACX,KAAK,MAAM,UAAU,IAAI,OAAO,EAAE,CAAC;QACjC,MAAM,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAC7D,CAAC;IAED,UAAU;IACV,oBAAoB,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,eAAe,CAC5B,UAA4E;IAE5E,OAAO,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,aAAa,CAC1B,UAAkD,EAClD,KAAkB,EAClB,MAAe,EACf,UAA4E;IAE5E,KAAK,CAAC,OAAO,EAAE,CAAC;IAChB,MAAM,QAAQ,GAAG,UAAU,CAAC,EAAE,CAAC;IAE/B,IAAI,CAAC;QACH,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,MAAM,oBAAoB,EAAE,CAAC;QAEnF,IAAI,WAAW,EAAE,CAAC;YAChB,KAAK,CAAC,WAAW,EAAE,CAAC;YACpB,OAAO,CAAC,GAAG,CACT,MAAM,KAAK,CAAC,OAAO,YAAY,QAAQ,aAAa,cAAc,WAAW,YAAY,EAAE,CAC5F,CAAC;YAEF,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,2BAA2B,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;gBACxD,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBAE5B,kBAAkB;gBAClB,MAAM,gBAAgB,EAAE,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB;;;;;WAKG;QACH,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5E,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;QACzD,OAAO,CAAC,KAAK,CAAC,eAAe,YAAY,EAAE,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"bulkRecordRepair.js","sourceRoot":"","sources":["../../../src/scripts/operations/bulkRecordRepair.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,oBAAoB,EACpB,oBAAoB,EACpB,2BAA2B,EAC3B,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAA4E,EAC5E,MAAe;IAEf,MAAM,KAAK,GAAgB;QACzB,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,CAAC;QACd,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,CAAC;QACT,MAAM,EAAE,EAAE;KACX,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtD,WAAW;IACX,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,SAAS,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC;IAE/C,WAAW;IACX,KAAK,MAAM,UAAU,IAAI,OAAO,EAAE,CAAC;QACjC,MAAM,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAC7D,CAAC;IAED,UAAU;IACV,oBAAoB,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,eAAe,CAC5B,UAA4E;IAE5E,8CAA8C;IAC9C,OAAO,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;AACpE,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,aAAa,CAC1B,UAAkD,EAClD,KAAkB,EAClB,MAAe,EACf,UAA4E;IAE5E,KAAK,CAAC,OAAO,EAAE,CAAC;IAChB,MAAM,QAAQ,GAAG,UAAU,CAAC,EAAE,CAAC;IAE/B,IAAI,CAAC;QACH,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,GAAG,MAAM,oBAAoB,EAAE,CAAC;QAEnF,IAAI,WAAW,EAAE,CAAC;YAChB,KAAK,CAAC,WAAW,EAAE,CAAC;YACpB,OAAO,CAAC,GAAG,CACT,MAAM,KAAK,CAAC,OAAO,YAAY,QAAQ,aAAa,cAAc,WAAW,YAAY,EAAE,CAC5F,CAAC;YAEF,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,2BAA2B,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;gBACxD,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBAE5B,kBAAkB;gBAClB,MAAM,gBAAgB,EAAE,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB;;;;;WAKG;QACH,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5E,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;QACzD,OAAO,CAAC,KAAK,CAAC,eAAe,YAAY,EAAE,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC"}
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.6.2
2
- // Built: 2026-10-11T14:15:49.129Z
1
+ // @exabugs/dynamodb-client v1.8.0
2
+ // Built: 2026-10-11T23:03:45.636Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -73,7 +73,7 @@ var init_types = __esm({
73
73
  function isAppError(error2) {
74
74
  return error2 instanceof AppError;
75
75
  }
76
- var AppError, AuthError, ConfigError, InvalidTokenError, ItemNotFoundError;
76
+ var AppError, AuthError, ConfigError, InvalidFilterError, InvalidTokenError, ItemNotFoundError;
77
77
  var init_errors = __esm({
78
78
  "src/shared/errors/index.ts"() {
79
79
  "use strict";
@@ -117,6 +117,14 @@ var init_errors = __esm({
117
117
  super("CONFIG_ERROR" /* CONFIG_ERROR */, message, 500, details);
118
118
  }
119
119
  };
120
+ InvalidFilterError = class extends AppError {
121
+ static {
122
+ __name(this, "InvalidFilterError");
123
+ }
124
+ constructor(message, details) {
125
+ super("INVALID_FILTER" /* INVALID_FILTER */, message, 400, details);
126
+ }
127
+ };
120
128
  InvalidTokenError = class extends AppError {
121
129
  static {
122
130
  __name(this, "InvalidTokenError");
@@ -34459,7 +34467,7 @@ function parseFilters(filter) {
34459
34467
  fieldKey,
34460
34468
  error: error2 instanceof Error ? error2.message : String(error2)
34461
34469
  });
34462
- throw new ConfigError(`Invalid filter field syntax: ${fieldKey}`, {
34470
+ throw new InvalidFilterError(`Invalid filter field syntax: ${fieldKey}`, {
34463
34471
  field: fieldKey,
34464
34472
  error: error2 instanceof Error ? error2.message : String(error2)
34465
34473
  });
@@ -34651,7 +34659,14 @@ async function executeInQuery(resource, targetIds, sort, requestId) {
34651
34659
  }
34652
34660
  async function executeAllRecordsQuery(resource, sort, perPage, nextToken, parsedFilters, requestId) {
34653
34661
  if (parsedFilters.length > 0) {
34654
- return executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken, parsedFilters, requestId);
34662
+ return executeAllRecordsQueryFullScan(
34663
+ resource,
34664
+ sort,
34665
+ perPage,
34666
+ nextToken,
34667
+ parsedFilters,
34668
+ requestId
34669
+ );
34655
34670
  }
34656
34671
  const dbClient2 = getDBClient();
34657
34672
  const tableName = getTableName();
@@ -34723,19 +34738,20 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34723
34738
  const dbClient2 = getDBClient();
34724
34739
  const tableName = getTableName();
34725
34740
  const costTracker = new CostTracker();
34726
- const offset = nextToken ? decodeOffsetToken(nextToken) ?? 0 : 0;
34727
- logger6.debug("Executing full scan query (ID sort)", {
34728
- requestId,
34729
- resource,
34730
- offset
34731
- });
34732
- const allRecords = [];
34733
34741
  let exclusiveStartKey;
34734
- let previousKeySignature;
34735
- const MAX_ITERATIONS = 1e4;
34742
+ if (nextToken) {
34743
+ const decoded = decodeNextToken(nextToken);
34744
+ exclusiveStartKey = { PK: decoded.PK, SK: decoded.SK };
34745
+ }
34746
+ const page = [];
34747
+ let lastReturned;
34748
+ let hasNextPage = false;
34749
+ let scanned = 0;
34736
34750
  let iterations = 0;
34737
- do {
34738
- if (++iterations > MAX_ITERATIONS) {
34751
+ let previousKeySignature;
34752
+ let chunkSize = SCAN_CHUNK_INITIAL;
34753
+ scan: while (true) {
34754
+ if (++iterations > SCAN_MAX_ITERATIONS) {
34739
34755
  logger6.error("executeAllRecordsQueryFullScan: \u6700\u5927\u53CD\u5FA9\u56DE\u6570\u3092\u8D85\u904E\u3057\u305F\u305F\u3081\u4E2D\u65AD", {
34740
34756
  requestId,
34741
34757
  resource,
@@ -34753,8 +34769,7 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34753
34769
  ":skPrefix": "id#"
34754
34770
  },
34755
34771
  ScanIndexForward: sort.order === "ASC",
34756
- Limit: 1e3,
34757
- // 大きめの Limit でループ回数を最小化
34772
+ Limit: chunkSize,
34758
34773
  ExclusiveStartKey: exclusiveStartKey,
34759
34774
  ConsistentRead: true,
34760
34775
  ReturnConsumedCapacity: "TOTAL"
@@ -34763,10 +34778,24 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34763
34778
  "Query"
34764
34779
  );
34765
34780
  costTracker.add(queryResult.ConsumedCapacity);
34766
- allRecords.push(...queryResult.Items || []);
34781
+ for (const row of queryResult.Items || []) {
34782
+ scanned++;
34783
+ if (!matchesAllFilters(extractCleanRecord(row), parsedFilters)) {
34784
+ continue;
34785
+ }
34786
+ if (page.length >= perPage) {
34787
+ hasNextPage = true;
34788
+ break scan;
34789
+ }
34790
+ page.push(extractCleanRecord(row));
34791
+ lastReturned = { PK: row.PK, SK: row.SK };
34792
+ }
34767
34793
  const nextKey = queryResult.LastEvaluatedKey;
34768
- const nextKeySignature = nextKey ? `${nextKey.PK}#${nextKey.SK}` : void 0;
34769
- if (nextKeySignature && nextKeySignature === previousKeySignature) {
34794
+ if (!nextKey) {
34795
+ break;
34796
+ }
34797
+ const nextKeySignature = `${nextKey.PK}#${nextKey.SK}`;
34798
+ if (nextKeySignature === previousKeySignature) {
34770
34799
  logger6.error("executeAllRecordsQueryFullScan: \u540C\u4E00\u306ELastEvaluatedKey\u304C\u8FD4\u5374\u3055\u308C\u305F\u305F\u3081\u4E2D\u65AD", {
34771
34800
  requestId,
34772
34801
  resource,
@@ -34776,28 +34805,24 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34776
34805
  }
34777
34806
  previousKeySignature = nextKeySignature;
34778
34807
  exclusiveStartKey = nextKey;
34779
- } while (exclusiveStartKey);
34780
- let items = allRecords.map((item) => extractCleanRecord(item));
34781
- items = items.filter((record) => matchesAllFilters(record, parsedFilters));
34782
- const page = items.slice(offset, offset + perPage);
34783
- const hasNextPage = offset + perPage < items.length;
34784
- const nextTokenValue = hasNextPage ? encodeOffsetToken(offset + perPage) : void 0;
34785
- logger6.info("ID full scan query succeeded", {
34808
+ chunkSize = Math.min(chunkSize * 2, SCAN_CHUNK_MAX);
34809
+ }
34810
+ const nextTokenValue = hasNextPage && lastReturned ? encodeNextToken(lastReturned.PK, lastReturned.SK) : void 0;
34811
+ logger6.info("ID filtered scan query succeeded", {
34786
34812
  requestId,
34787
34813
  resource,
34788
- totalMatched: items.length,
34789
- offset,
34814
+ scanned,
34790
34815
  returned: page.length,
34791
34816
  hasNextPage
34792
34817
  });
34793
34818
  return {
34794
34819
  items: page,
34795
- pageInfo: { hasNextPage, hasPreviousPage: offset > 0 },
34820
+ pageInfo: { hasNextPage, hasPreviousPage: !!nextToken },
34796
34821
  ...nextTokenValue && { nextToken: nextTokenValue },
34797
34822
  consumedCapacity: costTracker.getAggregated()
34798
34823
  };
34799
34824
  }
34800
- var import_lib_dynamodb2, logger6;
34825
+ var import_lib_dynamodb2, logger6, SCAN_CHUNK_INITIAL, SCAN_CHUNK_MAX, SCAN_MAX_ITERATIONS;
34801
34826
  var init_idQuery = __esm({
34802
34827
  "src/server/operations/find/idQuery.ts"() {
34803
34828
  "use strict";
@@ -34815,6 +34840,9 @@ var init_idQuery = __esm({
34815
34840
  __name(executeSpecificIdQuery, "executeSpecificIdQuery");
34816
34841
  __name(executeInQuery, "executeInQuery");
34817
34842
  __name(executeAllRecordsQuery, "executeAllRecordsQuery");
34843
+ SCAN_CHUNK_INITIAL = 100;
34844
+ SCAN_CHUNK_MAX = 1e3;
34845
+ SCAN_MAX_ITERATIONS = 1e4;
34818
34846
  __name(executeAllRecordsQueryFullScan, "executeAllRecordsQueryFullScan");
34819
34847
  }
34820
34848
  });
@@ -35918,8 +35946,8 @@ function addUpdateTimestamp(data) {
35918
35946
  }
35919
35947
  const now = (/* @__PURE__ */ new Date()).toISOString();
35920
35948
  return {
35921
- [timestampFields.updatedAt]: now,
35922
- ...data
35949
+ ...data,
35950
+ [timestampFields.updatedAt]: data[timestampFields.updatedAt] ?? now
35923
35951
  };
35924
35952
  }
35925
35953
  var init_timestamps = __esm({
@@ -36219,51 +36247,118 @@ var init_insertMany = __esm({
36219
36247
  }
36220
36248
  });
36221
36249
 
36222
- // src/server/operations/updateMany.ts
36223
- var updateMany_exports = {};
36224
- __export(updateMany_exports, {
36225
- handleUpdateMany: () => handleUpdateMany
36226
- });
36227
- function expandDotNotation(patch) {
36228
- const result = {};
36250
+ // src/server/utils/mergePatch.ts
36251
+ function isPlainObject(value) {
36252
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36253
+ }
36254
+ function invalidPath(path, reason) {
36255
+ return new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, `Invalid patch path "${path}": ${reason}`, 400, {
36256
+ path
36257
+ });
36258
+ }
36259
+ function mergeInto(target, patch) {
36229
36260
  for (const [key, value] of Object.entries(patch)) {
36230
36261
  if (key.includes(".")) {
36231
- const keys = key.split(".");
36232
- let current = result;
36233
- for (let i4 = 0; i4 < keys.length - 1; i4++) {
36234
- const k4 = keys[i4];
36235
- if (!(k4 in current)) {
36236
- current[k4] = {};
36237
- } else if (typeof current[k4] !== "object" || Array.isArray(current[k4])) {
36238
- current[k4] = {};
36239
- }
36240
- current = current[k4];
36241
- }
36242
- const lastKey = keys[keys.length - 1];
36243
- current[lastKey] = value;
36262
+ setByPath(target, key, value);
36244
36263
  } else {
36245
- result[key] = value;
36264
+ setKey(target, key, value, key);
36246
36265
  }
36247
36266
  }
36248
- return result;
36249
36267
  }
36250
- function applyJsonMergePatch(target, patch) {
36251
- const expandedPatch = expandDotNotation(patch);
36252
- const result = { ...target };
36253
- for (const [key, value] of Object.entries(expandedPatch)) {
36254
- if (value === null) {
36255
- delete result[key];
36256
- } else if (typeof value === "object" && !Array.isArray(value) && value !== null && typeof result[key] === "object" && !Array.isArray(result[key]) && result[key] !== null) {
36257
- result[key] = applyJsonMergePatch(
36258
- result[key],
36259
- value
36260
- );
36268
+ function setKey(target, key, value, path) {
36269
+ if (key === "__proto__") {
36270
+ throw invalidPath(path, "forbidden segment");
36271
+ }
36272
+ if (value === null) {
36273
+ delete target[key];
36274
+ } else if (isPlainObject(value) && isPlainObject(target[key])) {
36275
+ mergeInto(target[key], value);
36276
+ } else {
36277
+ target[key] = value;
36278
+ }
36279
+ }
36280
+ function setByPath(target, path, value) {
36281
+ const keys = path.split(".");
36282
+ if (keys.some((k4) => k4 === "")) {
36283
+ throw invalidPath(path, "empty segment");
36284
+ }
36285
+ let current = target;
36286
+ for (let i4 = 0; i4 < keys.length; i4++) {
36287
+ const key = keys[i4];
36288
+ const isLast = i4 === keys.length - 1;
36289
+ if (Array.isArray(current)) {
36290
+ if (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= current.length) {
36291
+ throw invalidPath(path, `"${key}" is not a valid index (length ${current.length})`);
36292
+ }
36293
+ const index = Number(key);
36294
+ if (isLast) {
36295
+ if (value === null) {
36296
+ throw invalidPath(path, "array elements cannot be deleted by path");
36297
+ }
36298
+ const existing = current[index];
36299
+ if (isPlainObject(value) && isPlainObject(existing)) {
36300
+ mergeInto(existing, value);
36301
+ } else {
36302
+ current[index] = value;
36303
+ }
36304
+ return;
36305
+ }
36306
+ const next2 = current[index];
36307
+ if (isPlainObject(next2) || Array.isArray(next2)) {
36308
+ current = next2;
36309
+ } else {
36310
+ if (value === null) {
36311
+ return;
36312
+ }
36313
+ const created = {};
36314
+ current[index] = created;
36315
+ current = created;
36316
+ }
36317
+ continue;
36318
+ }
36319
+ if (isLast) {
36320
+ setKey(current, key, value, path);
36321
+ return;
36322
+ }
36323
+ if (key === "__proto__") {
36324
+ throw invalidPath(path, "forbidden segment");
36325
+ }
36326
+ const next = current[key];
36327
+ if (isPlainObject(next) || Array.isArray(next)) {
36328
+ current = next;
36261
36329
  } else {
36262
- result[key] = value;
36330
+ if (value === null) {
36331
+ return;
36332
+ }
36333
+ const created = {};
36334
+ current[key] = created;
36335
+ current = created;
36263
36336
  }
36264
36337
  }
36338
+ }
36339
+ function applyJsonMergePatch(target, patch) {
36340
+ const result = structuredClone(target);
36341
+ mergeInto(result, structuredClone(patch));
36265
36342
  return result;
36266
36343
  }
36344
+ var init_mergePatch = __esm({
36345
+ "src/server/utils/mergePatch.ts"() {
36346
+ "use strict";
36347
+ init_shared();
36348
+ __name(isPlainObject, "isPlainObject");
36349
+ __name(invalidPath, "invalidPath");
36350
+ __name(mergeInto, "mergeInto");
36351
+ __name(setKey, "setKey");
36352
+ __name(setByPath, "setByPath");
36353
+ __name(applyJsonMergePatch, "applyJsonMergePatch");
36354
+ }
36355
+ });
36356
+
36357
+ // src/server/operations/updateMany.ts
36358
+ var updateMany_exports = {};
36359
+ __export(updateMany_exports, {
36360
+ handleUpdateMany: () => handleUpdateMany
36361
+ });
36267
36362
  async function handleUpdateMany(resource, params, requestId) {
36268
36363
  const { data: patchData, options } = params;
36269
36364
  const upsert = options?.upsert ?? false;
@@ -36391,11 +36486,9 @@ async function handleUpdateMany(resource, params, requestId) {
36391
36486
  const id = existingData.id;
36392
36487
  const oldShadowKeys = item.__shadowKeys ?? existingData.__shadowKeys ?? [];
36393
36488
  const actualPatchData2 = patchData.$set ? patchData.$set : patchData;
36394
- const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), actualPatchData2);
36395
- const updatedData = addUpdateTimestamp({
36396
- ...mergedData,
36397
- id
36398
- });
36489
+ const timestampedPatch = addUpdateTimestamp(actualPatchData2);
36490
+ const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), timestampedPatch);
36491
+ const updatedData = { ...mergedData, id };
36399
36492
  const newShadowRecords = generateShadowRecords(updatedData, resource, shadowConfig);
36400
36493
  const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
36401
36494
  const mainSK = generateMainRecordSK2(id);
@@ -36676,10 +36769,9 @@ var init_updateMany = __esm({
36676
36769
  init_chunking();
36677
36770
  init_cost_tracker();
36678
36771
  init_dynamodb3();
36772
+ init_mergePatch();
36679
36773
  init_timestamps();
36680
36774
  logger18 = createLogger({ service: "records-lambda" });
36681
- __name(expandDotNotation, "expandDotNotation");
36682
- __name(applyJsonMergePatch, "applyJsonMergePatch");
36683
36775
  __name(handleUpdateMany, "handleUpdateMany");
36684
36776
  __name(getPreparationErrorCode2, "getPreparationErrorCode");
36685
36777
  }
@@ -37115,6 +37207,9 @@ async function handleUpdateOne(resource, params, requestId) {
37115
37207
  if (updateManyResult.count === 0) {
37116
37208
  const error2 = Object.values(updateManyResult.errors || {})[0];
37117
37209
  if (error2) {
37210
+ if (error2.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */) {
37211
+ throw new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, error2.message ?? "Invalid patch", 400);
37212
+ }
37118
37213
  throw new Error(`Failed to update record: ${error2.message}`);
37119
37214
  } else {
37120
37215
  throw new Error(`Failed to update record: ${targetId}`);
@@ -37154,6 +37249,9 @@ async function handleUpdateOne(resource, params, requestId) {
37154
37249
  if (updateManyResult.count === 0) {
37155
37250
  const error2 = Object.values(updateManyResult.errors || {})[0];
37156
37251
  if (error2) {
37252
+ if (error2.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */) {
37253
+ throw new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, error2.message ?? "Invalid patch", 400);
37254
+ }
37157
37255
  throw new Error(`Failed to update record: ${error2.message}`);
37158
37256
  } else {
37159
37257
  throw new Error(`No records found matching filter`);
@@ -38706,7 +38804,7 @@ async function handler(event) {
38706
38804
  return createCorsResponse(HTTP_STATUS.OK);
38707
38805
  }
38708
38806
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
38709
- const version = "1.6.2";
38807
+ const version = "1.8.0";
38710
38808
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
38711
38809
  }
38712
38810
  if (event.requestContext.http.method !== "POST") {