@exabugs/dynamodb-client 1.6.2 → 1.7.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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.7.0] - 2026-10-12
11
+
12
+ ### Fixed
13
+
14
+ - 更新のドット記法パス(例: `sections.1.t`)が配列をオブジェクトに壊していた。配列は要素単位で更新し、範囲外・非数値の添字は 400 で拒否する。`__proto__` を含むパスも拒否する (#48)
15
+ - 更新時に `updatedAt` が変わらなかった(既存の値が優先されていた)。パッチ側に付与して毎回更新する。クライアントが値を指定した場合はそれを尊重し、`null` / `undefined` は未指定として扱う (#48)
16
+ - 解釈できないフィルタ指定が 500 (`CONFIG_ERROR`) になっていた。400 (`INVALID_FILTER`) を返す (#48)
17
+
18
+ ### Changed
19
+
20
+ - **`updatedAt` を明示しない更新で、値が毎回変わる**ようになった。`updatedAt` 順の一覧は更新のたびに並びが変わり、`updatedAt` のシャドーの再作成により 1 レコードあたりの書き込み項目が増える
21
+ - ドット記法で、存在しない(またはオブジェクト以外の)親に対する `null`(削除)は何もしなくなった(従来は `{a: null}` のような残骸が残っていた)
22
+
10
23
  ## [1.6.2] - 2026-10-11
11
24
 
12
25
  ### Security
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.6.2
2
- // Built: 2026-10-11T14:15:49.129Z
1
+ // @exabugs/dynamodb-client v1.7.0
2
+ // Built: 2026-10-11T16:38:03.698Z
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
  });
@@ -35918,8 +35926,8 @@ function addUpdateTimestamp(data) {
35918
35926
  }
35919
35927
  const now = (/* @__PURE__ */ new Date()).toISOString();
35920
35928
  return {
35921
- [timestampFields.updatedAt]: now,
35922
- ...data
35929
+ ...data,
35930
+ [timestampFields.updatedAt]: data[timestampFields.updatedAt] ?? now
35923
35931
  };
35924
35932
  }
35925
35933
  var init_timestamps = __esm({
@@ -36219,51 +36227,118 @@ var init_insertMany = __esm({
36219
36227
  }
36220
36228
  });
36221
36229
 
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 = {};
36230
+ // src/server/utils/mergePatch.ts
36231
+ function isPlainObject(value) {
36232
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36233
+ }
36234
+ function invalidPath(path, reason) {
36235
+ return new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, `Invalid patch path "${path}": ${reason}`, 400, {
36236
+ path
36237
+ });
36238
+ }
36239
+ function mergeInto(target, patch) {
36229
36240
  for (const [key, value] of Object.entries(patch)) {
36230
36241
  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;
36242
+ setByPath(target, key, value);
36244
36243
  } else {
36245
- result[key] = value;
36244
+ setKey(target, key, value, key);
36246
36245
  }
36247
36246
  }
36248
- return result;
36249
36247
  }
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
- );
36248
+ function setKey(target, key, value, path) {
36249
+ if (key === "__proto__") {
36250
+ throw invalidPath(path, "forbidden segment");
36251
+ }
36252
+ if (value === null) {
36253
+ delete target[key];
36254
+ } else if (isPlainObject(value) && isPlainObject(target[key])) {
36255
+ mergeInto(target[key], value);
36256
+ } else {
36257
+ target[key] = value;
36258
+ }
36259
+ }
36260
+ function setByPath(target, path, value) {
36261
+ const keys = path.split(".");
36262
+ if (keys.some((k4) => k4 === "")) {
36263
+ throw invalidPath(path, "empty segment");
36264
+ }
36265
+ let current = target;
36266
+ for (let i4 = 0; i4 < keys.length; i4++) {
36267
+ const key = keys[i4];
36268
+ const isLast = i4 === keys.length - 1;
36269
+ if (Array.isArray(current)) {
36270
+ if (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= current.length) {
36271
+ throw invalidPath(path, `"${key}" is not a valid index (length ${current.length})`);
36272
+ }
36273
+ const index = Number(key);
36274
+ if (isLast) {
36275
+ if (value === null) {
36276
+ throw invalidPath(path, "array elements cannot be deleted by path");
36277
+ }
36278
+ const existing = current[index];
36279
+ if (isPlainObject(value) && isPlainObject(existing)) {
36280
+ mergeInto(existing, value);
36281
+ } else {
36282
+ current[index] = value;
36283
+ }
36284
+ return;
36285
+ }
36286
+ const next2 = current[index];
36287
+ if (isPlainObject(next2) || Array.isArray(next2)) {
36288
+ current = next2;
36289
+ } else {
36290
+ if (value === null) {
36291
+ return;
36292
+ }
36293
+ const created = {};
36294
+ current[index] = created;
36295
+ current = created;
36296
+ }
36297
+ continue;
36298
+ }
36299
+ if (isLast) {
36300
+ setKey(current, key, value, path);
36301
+ return;
36302
+ }
36303
+ if (key === "__proto__") {
36304
+ throw invalidPath(path, "forbidden segment");
36305
+ }
36306
+ const next = current[key];
36307
+ if (isPlainObject(next) || Array.isArray(next)) {
36308
+ current = next;
36261
36309
  } else {
36262
- result[key] = value;
36310
+ if (value === null) {
36311
+ return;
36312
+ }
36313
+ const created = {};
36314
+ current[key] = created;
36315
+ current = created;
36263
36316
  }
36264
36317
  }
36318
+ }
36319
+ function applyJsonMergePatch(target, patch) {
36320
+ const result = structuredClone(target);
36321
+ mergeInto(result, structuredClone(patch));
36265
36322
  return result;
36266
36323
  }
36324
+ var init_mergePatch = __esm({
36325
+ "src/server/utils/mergePatch.ts"() {
36326
+ "use strict";
36327
+ init_shared();
36328
+ __name(isPlainObject, "isPlainObject");
36329
+ __name(invalidPath, "invalidPath");
36330
+ __name(mergeInto, "mergeInto");
36331
+ __name(setKey, "setKey");
36332
+ __name(setByPath, "setByPath");
36333
+ __name(applyJsonMergePatch, "applyJsonMergePatch");
36334
+ }
36335
+ });
36336
+
36337
+ // src/server/operations/updateMany.ts
36338
+ var updateMany_exports = {};
36339
+ __export(updateMany_exports, {
36340
+ handleUpdateMany: () => handleUpdateMany
36341
+ });
36267
36342
  async function handleUpdateMany(resource, params, requestId) {
36268
36343
  const { data: patchData, options } = params;
36269
36344
  const upsert = options?.upsert ?? false;
@@ -36391,11 +36466,9 @@ async function handleUpdateMany(resource, params, requestId) {
36391
36466
  const id = existingData.id;
36392
36467
  const oldShadowKeys = item.__shadowKeys ?? existingData.__shadowKeys ?? [];
36393
36468
  const actualPatchData2 = patchData.$set ? patchData.$set : patchData;
36394
- const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), actualPatchData2);
36395
- const updatedData = addUpdateTimestamp({
36396
- ...mergedData,
36397
- id
36398
- });
36469
+ const timestampedPatch = addUpdateTimestamp(actualPatchData2);
36470
+ const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), timestampedPatch);
36471
+ const updatedData = { ...mergedData, id };
36399
36472
  const newShadowRecords = generateShadowRecords(updatedData, resource, shadowConfig);
36400
36473
  const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
36401
36474
  const mainSK = generateMainRecordSK2(id);
@@ -36676,10 +36749,9 @@ var init_updateMany = __esm({
36676
36749
  init_chunking();
36677
36750
  init_cost_tracker();
36678
36751
  init_dynamodb3();
36752
+ init_mergePatch();
36679
36753
  init_timestamps();
36680
36754
  logger18 = createLogger({ service: "records-lambda" });
36681
- __name(expandDotNotation, "expandDotNotation");
36682
- __name(applyJsonMergePatch, "applyJsonMergePatch");
36683
36755
  __name(handleUpdateMany, "handleUpdateMany");
36684
36756
  __name(getPreparationErrorCode2, "getPreparationErrorCode");
36685
36757
  }
@@ -37115,6 +37187,9 @@ async function handleUpdateOne(resource, params, requestId) {
37115
37187
  if (updateManyResult.count === 0) {
37116
37188
  const error2 = Object.values(updateManyResult.errors || {})[0];
37117
37189
  if (error2) {
37190
+ if (error2.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */) {
37191
+ throw new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, error2.message ?? "Invalid patch", 400);
37192
+ }
37118
37193
  throw new Error(`Failed to update record: ${error2.message}`);
37119
37194
  } else {
37120
37195
  throw new Error(`Failed to update record: ${targetId}`);
@@ -37154,6 +37229,9 @@ async function handleUpdateOne(resource, params, requestId) {
37154
37229
  if (updateManyResult.count === 0) {
37155
37230
  const error2 = Object.values(updateManyResult.errors || {})[0];
37156
37231
  if (error2) {
37232
+ if (error2.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */) {
37233
+ throw new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, error2.message ?? "Invalid patch", 400);
37234
+ }
37157
37235
  throw new Error(`Failed to update record: ${error2.message}`);
37158
37236
  } else {
37159
37237
  throw new Error(`No records found matching filter`);
@@ -38706,7 +38784,7 @@ async function handler(event) {
38706
38784
  return createCorsResponse(HTTP_STATUS.OK);
38707
38785
  }
38708
38786
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
38709
- const version = "1.6.2";
38787
+ const version = "1.7.0";
38710
38788
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
38711
38789
  }
38712
38790
  if (event.requestContext.http.method !== "POST") {