@exabugs/dynamodb-client 1.3.39 → 1.3.41

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,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.3.41] - 2026-01-11
11
+
12
+ ### Added
13
+
14
+ - **updateOne filter版 upsert: true サポート**: filterで検索して存在しない場合に新規作成する機能を実装
15
+ - filter版で`upsert: true`オプションをサポート
16
+ - filterの条件を`$setOnInsert`に自動マージ
17
+ - 新規IDを自動生成(UUID)
18
+ - サーバー側ユニットテスト追加(8テスト)
19
+
20
+ ### Fixed
21
+
22
+ - **updateOne filter版 upsert: true バグ修正**: filterで検索して存在しない場合に`No records found matching filter`エラーが発生していた問題を修正
23
+ - `updateMany`のfilter版でレコードが見つからない場合の処理を実装
24
+ - 新規レコード作成時にfilterの条件を`$setOnInsert`にマージ
25
+ - FCMトークン登録(`{ token: "..." }`でupsert)が正常に動作するように修正
26
+
27
+ ### Changed
28
+
29
+ - **テストカバレッジ向上**: サーバー側の`updateOne`ユニットテストを追加(全707テスト成功)
30
+
31
+ ## [1.3.40] - 2026-01-11
32
+
33
+ ### Added
34
+
35
+ - **updateMany upsert: true サポート**: 存在しないレコードの新規作成機能を実装
36
+ - `upsert: true`オプションで存在しないレコードを自動作成
37
+ - `$set`と`$setOnInsert`オペレーターのサポート
38
+ - `$set`が`$setOnInsert`より優先される仕様
39
+ - サーバー側ユニットテスト追加(8テスト)
40
+
41
+ ### Fixed
42
+
43
+ - **updateMany upsert: true バグ修正**: 存在しないレコードで`ITEM_NOT_FOUND`エラーが発生していた問題を修正
44
+ - 新規レコード作成処理を実装
45
+ - `createdAt`/`updatedAt`タイムスタンプを自動追加
46
+ - シャドーレコードを自動生成
47
+ - レスポンス形式を修正(新規作成時は`$set`と`$setOnInsert`をマージ)
48
+
49
+ ### Changed
50
+
51
+ - **テストカバレッジ向上**: サーバー側の`updateMany`ユニットテストを追加
52
+ - 基本的な更新テスト
53
+ - `upsert: false`のテスト
54
+ - `upsert: true`(insert case)のテスト
55
+ - `upsert: true`(update case)のテスト
56
+ - `upsert: true`(混在ケース)のテスト
57
+
10
58
  ## [1.3.39] - 2026-01-11
11
59
 
12
60
  ### Fixed
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.3.39
2
- // Built: 2026-01-10T23:44:26.170Z
1
+ // @exabugs/dynamodb-client v1.3.41
2
+ // Built: 2026-01-11T01:29:54.903Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -31344,6 +31344,12 @@ var init_findOne = __esm({
31344
31344
  });
31345
31345
 
31346
31346
  // src/server/utils/timestamps.ts
31347
+ var timestamps_exports = {};
31348
+ __export(timestamps_exports, {
31349
+ addCreateTimestamps: () => addCreateTimestamps,
31350
+ addUpdateTimestamp: () => addUpdateTimestamp,
31351
+ getTimestampFields: () => getTimestampFields
31352
+ });
31347
31353
  function getTimestampFields() {
31348
31354
  const shadowConfig = getShadowConfig();
31349
31355
  return {
@@ -31711,14 +31717,36 @@ async function handleUpdateMany(resource, params, requestId) {
31711
31717
  resource,
31712
31718
  count: ids.length
31713
31719
  });
31714
- }
31715
- if (ids.length === 0) {
31716
- return {
31717
- count: 0,
31718
- successIds: {},
31719
- failedIds: {},
31720
- errors: {}
31721
- };
31720
+ if (ids.length === 0) {
31721
+ if (upsert) {
31722
+ const { randomUUID } = await import("crypto");
31723
+ const newId = randomUUID();
31724
+ ids = [newId];
31725
+ logger18.info("Creating new record with filter (upsert: true)", {
31726
+ requestId,
31727
+ resource,
31728
+ newId,
31729
+ filter: params.filter
31730
+ });
31731
+ if (patchData.$setOnInsert) {
31732
+ patchData.$setOnInsert = {
31733
+ ...params.filter,
31734
+ ...patchData.$setOnInsert
31735
+ };
31736
+ } else if (patchData.$set) {
31737
+ patchData.$setOnInsert = params.filter;
31738
+ } else {
31739
+ patchData.$setOnInsert = params.filter;
31740
+ }
31741
+ } else {
31742
+ return {
31743
+ count: 0,
31744
+ successIds: {},
31745
+ failedIds: {},
31746
+ errors: {}
31747
+ };
31748
+ }
31749
+ }
31722
31750
  }
31723
31751
  logLargeBatchWarning("updateMany", ids.length, requestId, resource);
31724
31752
  const dbClient2 = getDBClient();
@@ -31757,6 +31785,15 @@ async function handleUpdateMany(resource, params, requestId) {
31757
31785
  // 最初の10件のみログ
31758
31786
  });
31759
31787
  }
31788
+ if (upsert && notFoundIds.length > 0) {
31789
+ logger18.info("Creating new records (upsert: true)", {
31790
+ requestId,
31791
+ resource,
31792
+ notFoundCount: notFoundIds.length,
31793
+ notFoundIds: notFoundIds.slice(0, 10)
31794
+ // 最初の10件のみログ
31795
+ });
31796
+ }
31760
31797
  const shadowConfig = getShadowConfig();
31761
31798
  const preparedRecords = [];
31762
31799
  const preparationFailedIds = [];
@@ -31801,6 +31838,47 @@ async function handleUpdateMany(resource, params, requestId) {
31801
31838
  });
31802
31839
  }
31803
31840
  }
31841
+ if (upsert && notFoundIds.length > 0) {
31842
+ const { addCreateTimestamps: addCreateTimestamps2 } = await Promise.resolve().then(() => (init_timestamps(), timestamps_exports));
31843
+ for (const id of notFoundIds) {
31844
+ try {
31845
+ const setData = patchData.$set ? patchData.$set : patchData;
31846
+ const setOnInsertData2 = patchData.$setOnInsert ? patchData.$setOnInsert : {};
31847
+ const mergedData = {
31848
+ ...setOnInsertData2,
31849
+ ...setData,
31850
+ id
31851
+ };
31852
+ const newData = addCreateTimestamps2(mergedData);
31853
+ const newShadowRecords = generateShadowRecords(newData, resource, shadowConfig);
31854
+ const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
31855
+ const mainSK = generateMainRecordSK2(id);
31856
+ preparedRecords.push({
31857
+ id,
31858
+ updatedData: newData,
31859
+ oldShadowKeys: [],
31860
+ // 新規作成なので空
31861
+ newShadowKeys,
31862
+ mainSK
31863
+ });
31864
+ } catch (error2) {
31865
+ const errorMessage = error2 instanceof Error ? error2.message : "Unknown preparation error";
31866
+ const errorCode = getPreparationErrorCode2(error2);
31867
+ logger18.error("Failed to prepare new record for upsert", {
31868
+ requestId,
31869
+ recordId: id,
31870
+ error: errorMessage,
31871
+ errorCode
31872
+ });
31873
+ preparationFailedIds.push(id);
31874
+ preparationErrors.push({
31875
+ id,
31876
+ code: errorCode,
31877
+ message: errorMessage
31878
+ });
31879
+ }
31880
+ }
31881
+ }
31804
31882
  const getItemCount = /* @__PURE__ */ __name((record) => {
31805
31883
  const shadowDiff = calculateShadowDiff(record.oldShadowKeys, record.newShadowKeys);
31806
31884
  return 1 + shadowDiff.toDelete.length + shadowDiff.toAdd.length;
@@ -31875,26 +31953,37 @@ async function handleUpdateMany(resource, params, requestId) {
31875
31953
  const failedIdsMap = {};
31876
31954
  const errorsMap = {};
31877
31955
  const actualPatchData = patchData.$set ? patchData.$set : patchData;
31956
+ const setOnInsertData = patchData.$setOnInsert ? patchData.$setOnInsert : {};
31878
31957
  const successIdSet = new Set(successRecords.map((r4) => r4.id));
31879
31958
  for (let i4 = 0; i4 < ids.length; i4++) {
31880
31959
  const id = ids[i4];
31881
31960
  if (successIdSet.has(id)) {
31882
31961
  successIds[i4] = id;
31883
- items.push({
31884
- id,
31885
- ...actualPatchData
31886
- });
31962
+ if (upsert && notFoundIds.includes(id)) {
31963
+ items.push({
31964
+ id,
31965
+ ...setOnInsertData,
31966
+ ...actualPatchData
31967
+ });
31968
+ } else {
31969
+ items.push({
31970
+ id,
31971
+ ...actualPatchData
31972
+ });
31973
+ }
31887
31974
  }
31888
31975
  }
31889
- for (let i4 = 0; i4 < ids.length; i4++) {
31890
- const id = ids[i4];
31891
- if (notFoundIds.includes(id)) {
31892
- failedIdsMap[i4] = id;
31893
- errorsMap[i4] = {
31894
- id,
31895
- code: "ITEM_NOT_FOUND",
31896
- message: `Record not found: ${id}`
31897
- };
31976
+ if (!upsert) {
31977
+ for (let i4 = 0; i4 < ids.length; i4++) {
31978
+ const id = ids[i4];
31979
+ if (notFoundIds.includes(id)) {
31980
+ failedIdsMap[i4] = id;
31981
+ errorsMap[i4] = {
31982
+ id,
31983
+ code: "ITEM_NOT_FOUND",
31984
+ message: `Record not found: ${id}`
31985
+ };
31986
+ }
31898
31987
  }
31899
31988
  }
31900
31989
  for (let i4 = 0; i4 < ids.length; i4++) {
@@ -31934,9 +32023,12 @@ async function handleUpdateMany(resource, params, requestId) {
31934
32023
  completionRiskInfo,
31935
32024
  {
31936
32025
  updated: count,
31937
- notFound: notFoundIds.length,
32026
+ notFound: upsert ? 0 : notFoundIds.length,
32027
+ // upsert: true の場合は0
31938
32028
  preparationFailed: preparationFailedIds.length,
31939
- chunkExecutionFailed: chunkFailedIds.length
32029
+ chunkExecutionFailed: chunkFailedIds.length,
32030
+ upserted: upsert ? notFoundIds.length : 0
32031
+ // upsert: true の場合は新規作成数
31940
32032
  }
31941
32033
  );
31942
32034
  if (allFailedIds.length > 0) {
@@ -31949,7 +32041,8 @@ async function handleUpdateMany(resource, params, requestId) {
31949
32041
  allFailedIds.length,
31950
32042
  Object.values(errorsMap).map((e4) => e4.code),
31951
32043
  {
31952
- notFoundCount: notFoundIds.length,
32044
+ notFoundCount: upsert ? 0 : notFoundIds.length,
32045
+ // upsert: true の場合は0
31953
32046
  preparationFailures: preparationFailedIds.length,
31954
32047
  chunkExecutionFailures: chunkFailedIds.length
31955
32048
  }
@@ -33927,7 +34020,7 @@ async function handler(event) {
33927
34020
  return createCorsResponse(HTTP_STATUS.OK);
33928
34021
  }
33929
34022
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
33930
- const version = "1.3.39";
34023
+ const version = "1.3.41";
33931
34024
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
33932
34025
  }
33933
34026
  if (event.requestContext.http.method !== "POST") {