@exabugs/dynamodb-client 1.7.0 → 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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ 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
+
10
22
  ## [1.7.0] - 2026-10-12
11
23
 
12
24
  ### Fixed
@@ -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.7.0
2
- // Built: 2026-10-11T16:38:03.698Z
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;
@@ -34659,7 +34659,14 @@ async function executeInQuery(resource, targetIds, sort, requestId) {
34659
34659
  }
34660
34660
  async function executeAllRecordsQuery(resource, sort, perPage, nextToken, parsedFilters, requestId) {
34661
34661
  if (parsedFilters.length > 0) {
34662
- return executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken, parsedFilters, requestId);
34662
+ return executeAllRecordsQueryFullScan(
34663
+ resource,
34664
+ sort,
34665
+ perPage,
34666
+ nextToken,
34667
+ parsedFilters,
34668
+ requestId
34669
+ );
34663
34670
  }
34664
34671
  const dbClient2 = getDBClient();
34665
34672
  const tableName = getTableName();
@@ -34731,19 +34738,20 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34731
34738
  const dbClient2 = getDBClient();
34732
34739
  const tableName = getTableName();
34733
34740
  const costTracker = new CostTracker();
34734
- const offset = nextToken ? decodeOffsetToken(nextToken) ?? 0 : 0;
34735
- logger6.debug("Executing full scan query (ID sort)", {
34736
- requestId,
34737
- resource,
34738
- offset
34739
- });
34740
- const allRecords = [];
34741
34741
  let exclusiveStartKey;
34742
- let previousKeySignature;
34743
- 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;
34744
34750
  let iterations = 0;
34745
- do {
34746
- if (++iterations > MAX_ITERATIONS) {
34751
+ let previousKeySignature;
34752
+ let chunkSize = SCAN_CHUNK_INITIAL;
34753
+ scan: while (true) {
34754
+ if (++iterations > SCAN_MAX_ITERATIONS) {
34747
34755
  logger6.error("executeAllRecordsQueryFullScan: \u6700\u5927\u53CD\u5FA9\u56DE\u6570\u3092\u8D85\u904E\u3057\u305F\u305F\u3081\u4E2D\u65AD", {
34748
34756
  requestId,
34749
34757
  resource,
@@ -34761,8 +34769,7 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34761
34769
  ":skPrefix": "id#"
34762
34770
  },
34763
34771
  ScanIndexForward: sort.order === "ASC",
34764
- Limit: 1e3,
34765
- // 大きめの Limit でループ回数を最小化
34772
+ Limit: chunkSize,
34766
34773
  ExclusiveStartKey: exclusiveStartKey,
34767
34774
  ConsistentRead: true,
34768
34775
  ReturnConsumedCapacity: "TOTAL"
@@ -34771,10 +34778,24 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34771
34778
  "Query"
34772
34779
  );
34773
34780
  costTracker.add(queryResult.ConsumedCapacity);
34774
- 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
+ }
34775
34793
  const nextKey = queryResult.LastEvaluatedKey;
34776
- const nextKeySignature = nextKey ? `${nextKey.PK}#${nextKey.SK}` : void 0;
34777
- if (nextKeySignature && nextKeySignature === previousKeySignature) {
34794
+ if (!nextKey) {
34795
+ break;
34796
+ }
34797
+ const nextKeySignature = `${nextKey.PK}#${nextKey.SK}`;
34798
+ if (nextKeySignature === previousKeySignature) {
34778
34799
  logger6.error("executeAllRecordsQueryFullScan: \u540C\u4E00\u306ELastEvaluatedKey\u304C\u8FD4\u5374\u3055\u308C\u305F\u305F\u3081\u4E2D\u65AD", {
34779
34800
  requestId,
34780
34801
  resource,
@@ -34784,28 +34805,24 @@ async function executeAllRecordsQueryFullScan(resource, sort, perPage, nextToken
34784
34805
  }
34785
34806
  previousKeySignature = nextKeySignature;
34786
34807
  exclusiveStartKey = nextKey;
34787
- } while (exclusiveStartKey);
34788
- let items = allRecords.map((item) => extractCleanRecord(item));
34789
- items = items.filter((record) => matchesAllFilters(record, parsedFilters));
34790
- const page = items.slice(offset, offset + perPage);
34791
- const hasNextPage = offset + perPage < items.length;
34792
- const nextTokenValue = hasNextPage ? encodeOffsetToken(offset + perPage) : void 0;
34793
- 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", {
34794
34812
  requestId,
34795
34813
  resource,
34796
- totalMatched: items.length,
34797
- offset,
34814
+ scanned,
34798
34815
  returned: page.length,
34799
34816
  hasNextPage
34800
34817
  });
34801
34818
  return {
34802
34819
  items: page,
34803
- pageInfo: { hasNextPage, hasPreviousPage: offset > 0 },
34820
+ pageInfo: { hasNextPage, hasPreviousPage: !!nextToken },
34804
34821
  ...nextTokenValue && { nextToken: nextTokenValue },
34805
34822
  consumedCapacity: costTracker.getAggregated()
34806
34823
  };
34807
34824
  }
34808
- var import_lib_dynamodb2, logger6;
34825
+ var import_lib_dynamodb2, logger6, SCAN_CHUNK_INITIAL, SCAN_CHUNK_MAX, SCAN_MAX_ITERATIONS;
34809
34826
  var init_idQuery = __esm({
34810
34827
  "src/server/operations/find/idQuery.ts"() {
34811
34828
  "use strict";
@@ -34823,6 +34840,9 @@ var init_idQuery = __esm({
34823
34840
  __name(executeSpecificIdQuery, "executeSpecificIdQuery");
34824
34841
  __name(executeInQuery, "executeInQuery");
34825
34842
  __name(executeAllRecordsQuery, "executeAllRecordsQuery");
34843
+ SCAN_CHUNK_INITIAL = 100;
34844
+ SCAN_CHUNK_MAX = 1e3;
34845
+ SCAN_MAX_ITERATIONS = 1e4;
34826
34846
  __name(executeAllRecordsQueryFullScan, "executeAllRecordsQueryFullScan");
34827
34847
  }
34828
34848
  });
@@ -38784,7 +38804,7 @@ async function handler(event) {
38784
38804
  return createCorsResponse(HTTP_STATUS.OK);
38785
38805
  }
38786
38806
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
38787
- const version = "1.7.0";
38807
+ const version = "1.8.0";
38788
38808
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
38789
38809
  }
38790
38810
  if (event.requestContext.http.method !== "POST") {