@exabugs/dynamodb-client 1.24.1 → 1.25.1

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 (38) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/integrations/react-admin/dataProvider.d.ts +9 -0
  3. package/dist/integrations/react-admin/dataProvider.d.ts.map +1 -1
  4. package/dist/integrations/react-admin/dataProvider.js +24 -3
  5. package/dist/integrations/react-admin/dataProvider.js.map +1 -1
  6. package/dist/server/handler.cjs +400 -188
  7. package/dist/server/handler.cjs.map +4 -4
  8. package/dist/server/media-handler.cjs +17 -12
  9. package/dist/server/media-handler.cjs.map +3 -3
  10. package/dist/server/media-process-handler-dlq.cjs +17 -12
  11. package/dist/server/media-process-handler-dlq.cjs.map +3 -3
  12. package/dist/server/media-process-handler.cjs +17 -12
  13. package/dist/server/media-process-handler.cjs.map +3 -3
  14. package/dist/server/operations/deleteMany.d.ts.map +1 -1
  15. package/dist/server/operations/deleteMany.js +26 -2
  16. package/dist/server/operations/deleteMany.js.map +1 -1
  17. package/dist/server/operations/find/shadowQuery.d.ts.map +1 -1
  18. package/dist/server/operations/find/shadowQuery.js +59 -50
  19. package/dist/server/operations/find/shadowQuery.js.map +1 -1
  20. package/dist/server/operations/find/utils.d.ts.map +1 -1
  21. package/dist/server/operations/find/utils.js +19 -2
  22. package/dist/server/operations/find/utils.js.map +1 -1
  23. package/dist/server/operations/insertMany.d.ts.map +1 -1
  24. package/dist/server/operations/insertMany.js +36 -2
  25. package/dist/server/operations/insertMany.js.map +1 -1
  26. package/dist/server/operations/updateMany.d.ts.map +1 -1
  27. package/dist/server/operations/updateMany.js +134 -133
  28. package/dist/server/operations/updateMany.js.map +1 -1
  29. package/dist/server/utils/revision.d.ts +49 -0
  30. package/dist/server/utils/revision.d.ts.map +1 -0
  31. package/dist/server/utils/revision.js +92 -0
  32. package/dist/server/utils/revision.js.map +1 -0
  33. package/dist/server/utils/updateOperators.d.ts +25 -0
  34. package/dist/server/utils/updateOperators.d.ts.map +1 -0
  35. package/dist/server/utils/updateOperators.js +76 -0
  36. package/dist/server/utils/updateOperators.js.map +1 -0
  37. package/dist/sharp-layer.zip +0 -0
  38. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.24.1
2
- // Built: 2026-11-01T08:26:06.123Z
1
+ // @exabugs/dynamodb-client v1.25.1
2
+ // Built: 2026-11-13T23:34:00.545Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -3429,13 +3429,13 @@ var init_parse_utils = __esm({
3429
3429
  if (value === null || value === void 0) {
3430
3430
  return void 0;
3431
3431
  }
3432
- const asObject = expectObject(value);
3432
+ const asObject2 = expectObject(value);
3433
3433
  const setKeys = [];
3434
- for (const k5 in asObject) {
3435
- if (!hasOwn(asObject, k5)) {
3434
+ for (const k5 in asObject2) {
3435
+ if (!hasOwn(asObject2, k5)) {
3436
3436
  continue;
3437
3437
  }
3438
- if (asObject[k5] != null) {
3438
+ if (asObject2[k5] != null) {
3439
3439
  setKeys.push(k5);
3440
3440
  }
3441
3441
  }
@@ -3445,7 +3445,7 @@ var init_parse_utils = __esm({
3445
3445
  if (setKeys.length > 1) {
3446
3446
  throw new TypeError(`Unions must have exactly one non-null member. Keys ${setKeys} were not null.`);
3447
3447
  }
3448
- return asObject;
3448
+ return asObject2;
3449
3449
  }, "expectUnion");
3450
3450
  strictParseDouble = /* @__PURE__ */ __name((value) => {
3451
3451
  if (typeof value == "string") {
@@ -38724,22 +38724,24 @@ var require_main = __commonJS({
38724
38724
  var decode_bbox_int = /* @__PURE__ */ __name(function(hashInt, bitDepth) {
38725
38725
  bitDepth = bitDepth || 52;
38726
38726
  var maxLat = MAX_LAT, minLat = MIN_LAT, maxLon = MAX_LON, minLon = MIN_LON;
38727
- var latBit = 0, lonBit = 0;
38728
- var step = bitDepth / 2;
38729
- for (var i6 = 0; i6 < step; i6++) {
38730
- lonBit = get_bit(hashInt, (step - i6) * 2 - 1);
38731
- latBit = get_bit(hashInt, (step - i6) * 2 - 2);
38732
- if (latBit === 0) {
38733
- maxLat = (maxLat + minLat) / 2;
38734
- } else {
38735
- minLat = (maxLat + minLat) / 2;
38736
- }
38727
+ var lonBits = Math.ceil(bitDepth / 2);
38728
+ var latBits = Math.floor(bitDepth / 2);
38729
+ for (var i6 = 0; i6 < lonBits; i6++) {
38730
+ var lonBit = get_bit(hashInt, bitDepth - 1 - i6 * 2);
38737
38731
  if (lonBit === 0) {
38738
38732
  maxLon = (maxLon + minLon) / 2;
38739
38733
  } else {
38740
38734
  minLon = (maxLon + minLon) / 2;
38741
38735
  }
38742
38736
  }
38737
+ for (var j5 = 0; j5 < latBits; j5++) {
38738
+ var latBit = get_bit(hashInt, bitDepth - 2 - j5 * 2);
38739
+ if (latBit === 0) {
38740
+ maxLat = (maxLat + minLat) / 2;
38741
+ } else {
38742
+ minLat = (maxLat + minLat) / 2;
38743
+ }
38744
+ }
38743
38745
  return [minLat, minLon, maxLat, maxLon];
38744
38746
  }, "decode_bbox_int");
38745
38747
  function get_bit(bits, position) {
@@ -38844,6 +38846,9 @@ var require_main = __commonJS({
38844
38846
  return neighborHashIntList;
38845
38847
  }, "neighbors_int");
38846
38848
  var bboxes = /* @__PURE__ */ __name(function(minLat, minLon, maxLat, maxLon, numberOfChars) {
38849
+ if (numberOfChars <= 0) {
38850
+ throw new Error("numberOfChars must be strictly positive");
38851
+ }
38847
38852
  numberOfChars = numberOfChars || 9;
38848
38853
  var hashSouthWest = encode(minLat, minLon, numberOfChars);
38849
38854
  var hashNorthEast = encode(maxLat, maxLon, numberOfChars);
@@ -39030,16 +39035,9 @@ var init_shared = __esm({
39030
39035
  });
39031
39036
 
39032
39037
  // src/shared/constants/formatting.ts
39033
- var NUMBER_FORMAT;
39034
39038
  var init_formatting = __esm({
39035
39039
  "src/shared/constants/formatting.ts"() {
39036
39040
  "use strict";
39037
- NUMBER_FORMAT = {
39038
- /** シャドウSKで使用する数値の桁数(20桁ゼロ埋め) */
39039
- SHADOW_SK_DIGITS: 20,
39040
- /** ゼロ埋め文字 */
39041
- ZERO_PAD_CHAR: "0"
39042
- };
39043
39041
  }
39044
39042
  });
39045
39043
 
@@ -39752,6 +39750,71 @@ var init_cost_tracker = __esm({
39752
39750
  }
39753
39751
  });
39754
39752
 
39753
+ // src/server/utils/revision.ts
39754
+ function revisionOf(item) {
39755
+ const rev = item[REVISION_ATTRIBUTE];
39756
+ return typeof rev === "number" ? rev : void 0;
39757
+ }
39758
+ function nextRevision(current) {
39759
+ return (current ?? 0) + 1;
39760
+ }
39761
+ function notExistsCondition() {
39762
+ return { ConditionExpression: "attribute_not_exists(PK)" };
39763
+ }
39764
+ function unchangedCondition(revision) {
39765
+ const names = { "#rev": REVISION_ATTRIBUTE };
39766
+ if (revision === void 0) {
39767
+ return {
39768
+ ConditionExpression: "attribute_exists(PK) AND attribute_not_exists(#rev)",
39769
+ ExpressionAttributeNames: names
39770
+ };
39771
+ }
39772
+ return {
39773
+ ConditionExpression: "#rev = :rev",
39774
+ ExpressionAttributeNames: names,
39775
+ ExpressionAttributeValues: { ":rev": revision }
39776
+ };
39777
+ }
39778
+ function isWriteConflict(error2) {
39779
+ if (!error2 || typeof error2 !== "object") return false;
39780
+ const { name, CancellationReasons } = error2;
39781
+ if (name === "TransactionConflictException") return true;
39782
+ if (name !== "TransactionCanceledException" || !Array.isArray(CancellationReasons)) return false;
39783
+ const codes = CancellationReasons.map((r5) => r5?.Code).filter((c5) => c5 && c5 !== "None");
39784
+ return codes.length > 0 && codes.every((c5) => c5 === "ConditionalCheckFailed" || c5 === "TransactionConflict");
39785
+ }
39786
+ function sleep3(ms) {
39787
+ return new Promise((resolve) => setTimeout(resolve, ms));
39788
+ }
39789
+ async function writeWithConflictRetry(initial, write, refresh, baseDelayMs = 20) {
39790
+ let records = initial;
39791
+ for (let attempt = 0; ; attempt++) {
39792
+ try {
39793
+ if (records.length > 0) await write(records);
39794
+ return records;
39795
+ } catch (error2) {
39796
+ if (!isWriteConflict(error2) || attempt >= MAX_CONFLICT_RETRIES) throw error2;
39797
+ await sleep3(baseDelayMs * 2 ** attempt * (0.5 + Math.random() / 2));
39798
+ records = await refresh(records);
39799
+ }
39800
+ }
39801
+ }
39802
+ var REVISION_ATTRIBUTE, MAX_CONFLICT_RETRIES;
39803
+ var init_revision = __esm({
39804
+ "src/server/utils/revision.ts"() {
39805
+ "use strict";
39806
+ REVISION_ATTRIBUTE = "__rev";
39807
+ MAX_CONFLICT_RETRIES = 5;
39808
+ __name(revisionOf, "revisionOf");
39809
+ __name(nextRevision, "nextRevision");
39810
+ __name(notExistsCondition, "notExistsCondition");
39811
+ __name(unchangedCondition, "unchangedCondition");
39812
+ __name(isWriteConflict, "isWriteConflict");
39813
+ __name(sleep3, "sleep");
39814
+ __name(writeWithConflictRetry, "writeWithConflictRetry");
39815
+ }
39816
+ });
39817
+
39755
39818
  // src/server/utils/validation.ts
39756
39819
  function validateSortField(_config, _resource, _sort) {
39757
39820
  return;
@@ -40194,6 +40257,12 @@ function normalizeFindParams(config, resource, params) {
40194
40257
  schema: params.schema
40195
40258
  };
40196
40259
  }
40260
+ function withInferredType(parsed, fieldKey, value) {
40261
+ if (fieldKey.split(":").length >= 3) return parsed;
40262
+ if (typeof value === "number") return { ...parsed, type: "number" };
40263
+ if (typeof value === "boolean") return { ...parsed, type: "boolean" };
40264
+ return parsed;
40265
+ }
40197
40266
  function parseFilters(filter) {
40198
40267
  const parsedFilters = [];
40199
40268
  if (!filter || Object.keys(filter).length === 0) {
@@ -40204,11 +40273,14 @@ function parseFilters(filter) {
40204
40273
  if (value && typeof value === "object" && !Array.isArray(value)) {
40205
40274
  for (const [operator, operatorValue] of Object.entries(value)) {
40206
40275
  const parsed = parseFilterField(`${fieldKey}:${operator}`);
40207
- parsedFilters.push({ parsed, value: operatorValue });
40276
+ parsedFilters.push({
40277
+ parsed: withInferredType(parsed, fieldKey, operatorValue),
40278
+ value: operatorValue
40279
+ });
40208
40280
  }
40209
40281
  } else {
40210
40282
  const parsed = parseFilterField(fieldKey);
40211
- parsedFilters.push({ parsed, value });
40283
+ parsedFilters.push({ parsed: withInferredType(parsed, fieldKey, value), value });
40212
40284
  }
40213
40285
  } catch (error2) {
40214
40286
  logger7.error("Invalid filter field syntax", {
@@ -40295,6 +40367,7 @@ var init_utils6 = __esm({
40295
40367
  });
40296
40368
  __name(initializeFindConfig, "initializeFindConfig");
40297
40369
  __name(normalizeFindParams, "normalizeFindParams");
40370
+ __name(withInferredType, "withInferredType");
40298
40371
  __name(parseFilters, "parseFilters");
40299
40372
  __name(findOptimizableFilter, "findOptimizableFilter");
40300
40373
  __name(matchesAllFilters, "matchesAllFilters");
@@ -41092,51 +41165,39 @@ function buildKeyCondition(resource, sortField, optimizableFilter) {
41092
41165
  }
41093
41166
  const { operator, type } = optimizableFilter.parsed;
41094
41167
  const value = optimizableFilter.value;
41095
- const encodedValue = encodeValueForShadowSK(value, type);
41096
- const skValue = `${sortField}#${encodedValue}`;
41168
+ const { encoded, exact } = encodeValueForShadowSK(value, type, operator);
41169
+ const skValue = `${sortField}#${encoded}`;
41170
+ const afterValue = `${skValue}#id#~`;
41171
+ const beforeValue = `${skValue}#id#`;
41097
41172
  switch (operator) {
41098
41173
  case "$eq":
41099
41174
  return {
41100
41175
  keyConditionExpression: "PK = :pk AND begins_with(SK, :skValue)",
41101
41176
  expressionAttributeValues: {
41102
41177
  ...expressionAttributeValues,
41103
- ":skValue": `${skValue}#id#`
41178
+ ":skValue": beforeValue
41104
41179
  }
41105
41180
  };
41181
+ // 範囲検索は SK BETWEEN の 1 条件にする。
41182
+ // - DynamoDB はソートキーに 1 つの条件しか許さない(begins_with と比較を並べると ValidationException)
41183
+ // - 上限・下限を sortField の範囲に閉じないと、他の項目のシャドー(や本体 id#…)まで読む
41184
+ // BETWEEN は両端を含むが、端の値(`…#id#`、`…#id#~`、`{sortField}$`)と一致する SK は実在しない
41106
41185
  case "$gt":
41107
- return {
41108
- keyConditionExpression: "PK = :pk AND SK > :skValue",
41109
- expressionAttributeValues: {
41110
- ...expressionAttributeValues,
41111
- ":skValue": `${skValue}#id#~`
41112
- }
41113
- };
41186
+ return rangeCondition(
41187
+ expressionAttributeValues,
41188
+ exact ? afterValue : beforeValue,
41189
+ fieldUpperBound(sortField)
41190
+ );
41114
41191
  case "$gte":
41115
- return {
41116
- keyConditionExpression: "PK = :pk AND SK >= :skValue",
41117
- expressionAttributeValues: {
41118
- ...expressionAttributeValues,
41119
- ":skValue": `${skValue}#id#`
41120
- }
41121
- };
41192
+ return rangeCondition(expressionAttributeValues, beforeValue, fieldUpperBound(sortField));
41122
41193
  case "$lt":
41123
- return {
41124
- keyConditionExpression: "PK = :pk AND begins_with(SK, :skPrefix) AND SK < :skValue",
41125
- expressionAttributeValues: {
41126
- ...expressionAttributeValues,
41127
- ":skPrefix": `${sortField}#`,
41128
- ":skValue": `${skValue}#id#`
41129
- }
41130
- };
41194
+ return rangeCondition(
41195
+ expressionAttributeValues,
41196
+ `${sortField}#`,
41197
+ exact ? beforeValue : afterValue
41198
+ );
41131
41199
  case "$lte":
41132
- return {
41133
- keyConditionExpression: "PK = :pk AND begins_with(SK, :skPrefix) AND SK <= :skValue",
41134
- expressionAttributeValues: {
41135
- ...expressionAttributeValues,
41136
- ":skPrefix": `${sortField}#`,
41137
- ":skValue": `${skValue}#id#~`
41138
- }
41139
- };
41200
+ return rangeCondition(expressionAttributeValues, `${sortField}#`, afterValue);
41140
41201
  case "$starts":
41141
41202
  return {
41142
41203
  keyConditionExpression: "PK = :pk AND begins_with(SK, :skValue)",
@@ -41155,16 +41216,35 @@ function buildKeyCondition(resource, sortField, optimizableFilter) {
41155
41216
  };
41156
41217
  }
41157
41218
  }
41158
- function encodeValueForShadowSK(value, type) {
41159
- if (type === "number") {
41160
- return String(value).padStart(NUMBER_FORMAT.SHADOW_SK_DIGITS, NUMBER_FORMAT.ZERO_PAD_CHAR);
41161
- } else if (type === "date") {
41162
- return new Date(String(value)).toISOString();
41163
- } else if (type === "boolean") {
41164
- return String(value);
41165
- } else {
41166
- return String(value).replace(/#/g, "##").replace(/ /g, "#");
41219
+ function fieldUpperBound(sortField) {
41220
+ return `${sortField}$`;
41221
+ }
41222
+ function rangeCondition(base, low, high) {
41223
+ return {
41224
+ keyConditionExpression: "PK = :pk AND SK BETWEEN :skLow AND :skHigh",
41225
+ expressionAttributeValues: { ...base, ":skLow": low, ":skHigh": high }
41226
+ };
41227
+ }
41228
+ function encodeValueForShadowSK(value, type, operator) {
41229
+ const config = getShadowConfig();
41230
+ if (type === "number" || typeof value === "number") {
41231
+ const num = Number(value);
41232
+ return { encoded: formatFieldValue2("number", num, config), exact: false };
41167
41233
  }
41234
+ if (type === "boolean" || typeof value === "boolean") {
41235
+ const bool = typeof value === "boolean" ? value : String(value).toLowerCase() === "true";
41236
+ return { encoded: formatFieldValue2("boolean", bool, config), exact: true };
41237
+ }
41238
+ const str = String(value);
41239
+ if (operator !== "$starts" && (type === "date" || inferFieldType(str) === "datetime")) {
41240
+ return { encoded: formatFieldValue2("datetime", str, config), exact: true };
41241
+ }
41242
+ const encoded = formatFieldValue2("string", str, config);
41243
+ const bytes = new TextEncoder().encode(str).length;
41244
+ return {
41245
+ encoded,
41246
+ exact: encoded === escapeString2(str) && bytes < config.stringMaxBytes - 3
41247
+ };
41168
41248
  }
41169
41249
  function extractRecordIds(shadowRecords) {
41170
41250
  return shadowRecords.map((record) => {
@@ -41191,8 +41271,8 @@ var init_shadowQuery = __esm({
41191
41271
  "src/server/operations/find/shadowQuery.ts"() {
41192
41272
  "use strict";
41193
41273
  import_lib_dynamodb5 = __toESM(require_dist_cjs22(), 1);
41194
- init_formatting();
41195
41274
  init_shared();
41275
+ init_shadow();
41196
41276
  init_batchGet();
41197
41277
  init_cost_tracker();
41198
41278
  init_dynamodb3();
@@ -41212,6 +41292,8 @@ var init_shadowQuery = __esm({
41212
41292
  __name(sortInMemory, "sortInMemory");
41213
41293
  __name(executeShadowRecordQuery, "executeShadowRecordQuery");
41214
41294
  __name(buildKeyCondition, "buildKeyCondition");
41295
+ __name(fieldUpperBound, "fieldUpperBound");
41296
+ __name(rangeCondition, "rangeCondition");
41215
41297
  __name(encodeValueForShadowSK, "encodeValueForShadowSK");
41216
41298
  __name(extractRecordIds, "extractRecordIds");
41217
41299
  __name(fetchMainRecords, "fetchMainRecords");
@@ -41406,7 +41488,8 @@ async function handleDeleteMany(resource, params, requestId) {
41406
41488
  preparedRecords.push({
41407
41489
  id,
41408
41490
  mainSK: generateMainRecordSK2(id),
41409
- shadowKeys
41491
+ shadowKeys,
41492
+ revision: revisionOf(item)
41410
41493
  });
41411
41494
  }
41412
41495
  const notFoundIds = ids.filter((id) => !existingIds.has(id));
@@ -41454,16 +41537,20 @@ async function handleDeleteMany(resource, params, requestId) {
41454
41537
  chunks.length,
41455
41538
  preparationRiskInfo
41456
41539
  );
41457
- const executeChunk = /* @__PURE__ */ __name(async (chunk) => {
41540
+ const writeChunk = /* @__PURE__ */ __name(async (chunk) => {
41458
41541
  const transactItems = [];
41459
41542
  for (const record of chunk) {
41543
+ if (record.gone) continue;
41460
41544
  transactItems.push({
41461
41545
  Delete: {
41462
41546
  TableName: tableName,
41463
41547
  Key: {
41464
41548
  PK: resource,
41465
41549
  SK: record.mainSK
41466
- }
41550
+ },
41551
+ // 読んでから消すまでの間に、ほかの書き込みが入っていないこと(楽観ロック)。
41552
+ // 古い一覧のままシャドーを消すと、新しく増えたシャドーが残る
41553
+ ...unchangedCondition(record.revision)
41467
41554
  }
41468
41555
  });
41469
41556
  for (const shadowSK of record.shadowKeys) {
@@ -41478,6 +41565,7 @@ async function handleDeleteMany(resource, params, requestId) {
41478
41565
  });
41479
41566
  }
41480
41567
  }
41568
+ if (transactItems.length === 0) return;
41481
41569
  const result = await executeDynamoDBOperation(
41482
41570
  () => dbClient2.send(
41483
41571
  new import_lib_dynamodb6.TransactWriteCommand({
@@ -41494,8 +41582,24 @@ async function handleDeleteMany(resource, params, requestId) {
41494
41582
  } else {
41495
41583
  costTracker.add(result.ConsumedCapacity);
41496
41584
  }
41497
- return chunk;
41498
- }, "executeChunk");
41585
+ }, "writeChunk");
41586
+ const executeChunk = /* @__PURE__ */ __name((chunk) => writeWithConflictRetry(chunk, writeChunk, async (stale) => {
41587
+ const items = await batchGetAll(
41588
+ stale.map((record) => ({ PK: resource, SK: record.mainSK })),
41589
+ { consistentRead: true, costTracker, requestId }
41590
+ );
41591
+ const fresh = new Map(items.map((item) => [item.SK, item]));
41592
+ return stale.map((record) => {
41593
+ const item = fresh.get(record.mainSK);
41594
+ if (!item) return { ...record, gone: true };
41595
+ const data = item.data;
41596
+ return {
41597
+ ...record,
41598
+ shadowKeys: item.__shadowKeys ?? data.__shadowKeys ?? [],
41599
+ revision: revisionOf(item)
41600
+ };
41601
+ });
41602
+ }), "executeChunk");
41499
41603
  const {
41500
41604
  successRecords,
41501
41605
  failedIds: chunkFailedIds,
@@ -41587,6 +41691,7 @@ var init_deleteMany = __esm({
41587
41691
  init_chunking();
41588
41692
  init_cost_tracker();
41589
41693
  init_dynamodb3();
41694
+ init_revision();
41590
41695
  init_findAll();
41591
41696
  logger12 = createLogger({ service: "records-lambda" });
41592
41697
  __name(handleDeleteMany, "handleDeleteMany");
@@ -41881,7 +41986,7 @@ async function handleInsertMany(resource, params, requestId) {
41881
41986
  chunks.length,
41882
41987
  preparationRiskInfo
41883
41988
  );
41884
- const executeChunk = /* @__PURE__ */ __name(async (chunk) => {
41989
+ const writeChunk = /* @__PURE__ */ __name(async (chunk) => {
41885
41990
  const transactItems = [];
41886
41991
  for (const record of chunk) {
41887
41992
  transactItems.push({
@@ -41891,12 +41996,23 @@ async function handleInsertMany(resource, params, requestId) {
41891
41996
  PK: resource,
41892
41997
  SK: record.mainSK,
41893
41998
  __shadowKeys: record.shadowKeys,
41999
+ [REVISION_ATTRIBUTE]: nextRevision(record.existing?.revision),
41894
42000
  data: {
41895
42001
  ...record.fullRecordData
41896
42002
  }
41897
- }
42003
+ },
42004
+ // 既存の id への挿入は上書きになる。既存を知らずに上書きすると、古いシャドーが残るので、
42005
+ // まず「無い」ことを条件に書き、外れたら既存を読んで、古いシャドーも一緒に消す
42006
+ ...record.existing ? unchangedCondition(record.existing.revision) : notExistsCondition()
41898
42007
  }
41899
42008
  });
42009
+ const newShadowKeys = new Set(record.shadowKeys);
42010
+ for (const shadowSK of record.existing?.shadowKeys ?? []) {
42011
+ if (newShadowKeys.has(shadowSK)) continue;
42012
+ transactItems.push({
42013
+ Delete: { TableName: tableName, Key: { PK: resource, SK: shadowSK } }
42014
+ });
42015
+ }
41900
42016
  for (const shadowSK of record.shadowKeys) {
41901
42017
  transactItems.push({
41902
42018
  Put: {
@@ -41926,8 +42042,26 @@ async function handleInsertMany(resource, params, requestId) {
41926
42042
  } else {
41927
42043
  costTracker.add(result.ConsumedCapacity);
41928
42044
  }
41929
- return chunk;
41930
- }, "executeChunk");
42045
+ }, "writeChunk");
42046
+ const executeChunk = /* @__PURE__ */ __name((chunk) => writeWithConflictRetry(chunk, writeChunk, async (stale) => {
42047
+ const items = await batchGetAll(
42048
+ stale.map((record) => ({ PK: resource, SK: record.mainSK })),
42049
+ { consistentRead: true, costTracker, requestId }
42050
+ );
42051
+ const fresh = new Map(items.map((item) => [item.SK, item]));
42052
+ return stale.map((record) => {
42053
+ const item = fresh.get(record.mainSK);
42054
+ if (!item) return { ...record, existing: void 0 };
42055
+ const data = item.data;
42056
+ return {
42057
+ ...record,
42058
+ existing: {
42059
+ revision: revisionOf(item),
42060
+ shadowKeys: item.__shadowKeys ?? data?.__shadowKeys ?? []
42061
+ }
42062
+ };
42063
+ });
42064
+ }), "executeChunk");
41931
42065
  const {
41932
42066
  successRecords,
41933
42067
  failedIds: chunkFailedIds,
@@ -42027,10 +42161,12 @@ var init_insertMany = __esm({
42027
42161
  init_shared();
42028
42162
  init_shadow();
42029
42163
  init_shadow();
42164
+ init_batchGet();
42030
42165
  init_bulkOperations();
42031
42166
  init_chunking();
42032
42167
  init_cost_tracker();
42033
42168
  init_dynamodb3();
42169
+ init_revision();
42034
42170
  init_timestamps();
42035
42171
  init_ttl();
42036
42172
  logger18 = createLogger({ service: "records-lambda" });
@@ -42146,6 +42282,71 @@ var init_mergePatch = __esm({
42146
42282
  }
42147
42283
  });
42148
42284
 
42285
+ // src/server/utils/updateOperators.ts
42286
+ function invalid(message) {
42287
+ return new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message, 400);
42288
+ }
42289
+ function asObject(value, operator) {
42290
+ if (value === void 0) return {};
42291
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
42292
+ throw invalid(`${operator} must be an object`);
42293
+ }
42294
+ return value;
42295
+ }
42296
+ function unsetFields(value) {
42297
+ if (value === void 0) return [];
42298
+ if (Array.isArray(value)) {
42299
+ if (!value.every((f5) => typeof f5 === "string")) {
42300
+ throw invalid("$unset must be an array of field names or an object");
42301
+ }
42302
+ return value;
42303
+ }
42304
+ return Object.keys(asObject(value, "$unset"));
42305
+ }
42306
+ function resolveUpdateOperators(data) {
42307
+ const operators = Object.keys(data).filter((key) => key.startsWith("$"));
42308
+ if (operators.length === 0) {
42309
+ return { patch: data, setOnInsert: {}, returned: data };
42310
+ }
42311
+ const unsupported = operators.filter((op2) => !SUPPORTED_OPERATORS.includes(op2));
42312
+ if (unsupported.length > 0) {
42313
+ throw invalid(
42314
+ `Unsupported update operator: ${unsupported.join(", ")} (supported: ${SUPPORTED_OPERATORS.join(", ")})`
42315
+ );
42316
+ }
42317
+ const set = asObject(data.$set, "$set");
42318
+ const patch = { ...set };
42319
+ for (const field of unsetFields(data.$unset)) {
42320
+ if (field in set) {
42321
+ throw invalid(`Field "${field}" is in both $set and $unset`);
42322
+ }
42323
+ patch[field] = null;
42324
+ }
42325
+ return { patch, setOnInsert: asObject(data.$setOnInsert, "$setOnInsert"), returned: set };
42326
+ }
42327
+ function equalityFieldsOf(filter) {
42328
+ const fields = {};
42329
+ for (const [key, value] of Object.entries(filter)) {
42330
+ if (key.includes(":") || key.startsWith("$")) continue;
42331
+ if (typeof value === "object" && value !== null) continue;
42332
+ fields[key] = value;
42333
+ }
42334
+ return fields;
42335
+ }
42336
+ var SUPPORTED_OPERATORS;
42337
+ var init_updateOperators = __esm({
42338
+ "src/server/utils/updateOperators.ts"() {
42339
+ "use strict";
42340
+ init_shared();
42341
+ SUPPORTED_OPERATORS = ["$set", "$setOnInsert", "$unset"];
42342
+ __name(invalid, "invalid");
42343
+ __name(asObject, "asObject");
42344
+ __name(unsetFields, "unsetFields");
42345
+ __name(resolveUpdateOperators, "resolveUpdateOperators");
42346
+ __name(equalityFieldsOf, "equalityFieldsOf");
42347
+ }
42348
+ });
42349
+
42149
42350
  // src/server/operations/updateMany.ts
42150
42351
  var updateMany_exports = {};
42151
42352
  __export(updateMany_exports, {
@@ -42156,6 +42357,7 @@ async function handleUpdateMany(resource, params, requestId) {
42156
42357
  const upsert = options?.upsert ?? false;
42157
42358
  const startTime = Date.now();
42158
42359
  const costTracker = new CostTracker();
42360
+ const update = resolveUpdateOperators(patchData);
42159
42361
  logger20.debug("Executing updateMany", {
42160
42362
  requestId,
42161
42363
  resource,
@@ -42203,16 +42405,10 @@ async function handleUpdateMany(resource, params, requestId) {
42203
42405
  newId,
42204
42406
  filter: params.filter
42205
42407
  });
42206
- if (patchData.$setOnInsert) {
42207
- patchData.$setOnInsert = {
42208
- ...params.filter,
42209
- ...patchData.$setOnInsert
42210
- };
42211
- } else if (patchData.$set) {
42212
- patchData.$setOnInsert = params.filter;
42213
- } else {
42214
- patchData.$setOnInsert = params.filter;
42215
- }
42408
+ update.setOnInsert = {
42409
+ ...equalityFieldsOf(params.filter),
42410
+ ...update.setOnInsert
42411
+ };
42216
42412
  } else {
42217
42413
  return {
42218
42414
  count: 0,
@@ -42227,118 +42423,121 @@ async function handleUpdateMany(resource, params, requestId) {
42227
42423
  logLargeBatchWarning("updateMany", ids.length, requestId, resource);
42228
42424
  const dbClient2 = getDBClient();
42229
42425
  const tableName = getTableName();
42230
- const existingItems = await batchGetAll(
42231
- ids.map((id) => ({ PK: resource, SK: generateMainRecordSK2(id) })),
42232
- { consistentRead: true, costTracker, requestId }
42233
- );
42234
- const existingIds = new Set(
42235
- existingItems.map((item) => {
42236
- const data = item.data;
42237
- return data.id;
42238
- })
42239
- );
42240
- const notFoundIds = ids.filter((id) => !existingIds.has(id));
42241
- if (!upsert && notFoundIds.length > 0) {
42242
- logger20.warn("Records not found (upsert: false)", {
42243
- requestId,
42244
- resource,
42245
- notFoundCount: notFoundIds.length,
42246
- notFoundIds: notFoundIds.slice(0, 10)
42247
- // 最初の10件のみログ
42248
- });
42249
- }
42250
- if (upsert && notFoundIds.length > 0) {
42251
- logger20.info("Creating new records (upsert: true)", {
42252
- requestId,
42253
- resource,
42254
- notFoundCount: notFoundIds.length,
42255
- notFoundIds: notFoundIds.slice(0, 10)
42256
- // 最初の10件のみログ
42257
- });
42258
- }
42259
- const shadowConfig = getShadowConfig();
42260
- const preparedRecords = [];
42261
- const preparationFailedIds = [];
42262
- const preparationErrors = [];
42263
- for (const item of existingItems) {
42264
- try {
42265
- const existingData = item.data;
42266
- const id = existingData.id;
42267
- const oldShadowKeys = item.__shadowKeys ?? existingData.__shadowKeys ?? [];
42268
- const actualPatchData2 = patchData.$set ? patchData.$set : patchData;
42269
- const timestampedPatch = addUpdateTimestamp(actualPatchData2);
42270
- const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), timestampedPatch);
42271
- const updatedData = { ...mergedData, id };
42272
- const newShadowRecords = generateShadowRecords(updatedData, resource, shadowConfig);
42273
- const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
42274
- const mainSK = generateMainRecordSK2(id);
42275
- preparedRecords.push({
42276
- id,
42277
- updatedData,
42278
- oldShadowKeys,
42279
- newShadowKeys,
42280
- mainSK
42281
- });
42282
- } catch (error2) {
42283
- const existingData = item.data;
42284
- const failedId = existingData.id || "unknown-id";
42285
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown preparation error";
42286
- const errorCode = getPreparationErrorCode2(error2);
42287
- logger20.error("Failed to prepare record for update", {
42426
+ const prepare = /* @__PURE__ */ __name(async (targetIds) => {
42427
+ const existingItems = await batchGetAll(
42428
+ targetIds.map((id) => ({ PK: resource, SK: generateMainRecordSK2(id) })),
42429
+ { consistentRead: true, costTracker, requestId }
42430
+ );
42431
+ const existingIds = new Set(
42432
+ existingItems.map((item) => {
42433
+ const data = item.data;
42434
+ return data.id;
42435
+ })
42436
+ );
42437
+ const notFoundIds2 = targetIds.filter((id) => !existingIds.has(id));
42438
+ if (!upsert && notFoundIds2.length > 0) {
42439
+ logger20.warn("Records not found (upsert: false)", {
42288
42440
  requestId,
42289
- recordId: failedId,
42290
- error: errorMessage,
42291
- errorCode
42441
+ resource,
42442
+ notFoundCount: notFoundIds2.length,
42443
+ notFoundIds: notFoundIds2.slice(0, 10)
42444
+ // 最初の10件のみログ
42292
42445
  });
42293
- preparationFailedIds.push(failedId);
42294
- preparationErrors.push({
42295
- id: failedId,
42296
- code: errorCode,
42297
- message: errorMessage
42446
+ }
42447
+ if (upsert && notFoundIds2.length > 0) {
42448
+ logger20.info("Creating new records (upsert: true)", {
42449
+ requestId,
42450
+ resource,
42451
+ notFoundCount: notFoundIds2.length,
42452
+ notFoundIds: notFoundIds2.slice(0, 10)
42453
+ // 最初の10件のみログ
42298
42454
  });
42299
42455
  }
42300
- }
42301
- if (upsert && notFoundIds.length > 0) {
42302
- const { addCreateTimestamps: addCreateTimestamps2 } = await Promise.resolve().then(() => (init_timestamps(), timestamps_exports));
42303
- for (const id of notFoundIds) {
42456
+ const shadowConfig = getShadowConfig();
42457
+ const preparedRecords2 = [];
42458
+ const preparationFailedIds2 = [];
42459
+ const preparationErrors2 = [];
42460
+ for (const item of existingItems) {
42304
42461
  try {
42305
- const setData = patchData.$set ? patchData.$set : patchData;
42306
- const setOnInsertData2 = patchData.$setOnInsert ? patchData.$setOnInsert : {};
42307
- const mergedData = {
42308
- ...setOnInsertData2,
42309
- ...setData,
42310
- id
42311
- };
42312
- const newData = addCreateTimestamps2(mergedData);
42313
- const newShadowRecords = generateShadowRecords(newData, resource, shadowConfig);
42462
+ const existingData = item.data;
42463
+ const id = existingData.id;
42464
+ const oldShadowKeys = item.__shadowKeys ?? existingData.__shadowKeys ?? [];
42465
+ const timestampedPatch = addUpdateTimestamp(update.patch);
42466
+ const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), timestampedPatch);
42467
+ const updatedData = { ...mergedData, id };
42468
+ const newShadowRecords = generateShadowRecords(updatedData, resource, shadowConfig);
42314
42469
  const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
42315
42470
  const mainSK = generateMainRecordSK2(id);
42316
- preparedRecords.push({
42471
+ preparedRecords2.push({
42317
42472
  id,
42318
- updatedData: newData,
42319
- oldShadowKeys: [],
42320
- // 新規作成なので空
42473
+ updatedData,
42474
+ oldShadowKeys,
42321
42475
  newShadowKeys,
42322
- mainSK
42476
+ mainSK,
42477
+ isNew: false,
42478
+ revision: revisionOf(item)
42323
42479
  });
42324
42480
  } catch (error2) {
42481
+ const existingData = item.data;
42482
+ const failedId = existingData.id || "unknown-id";
42325
42483
  const errorMessage = error2 instanceof Error ? error2.message : "Unknown preparation error";
42326
42484
  const errorCode = getPreparationErrorCode2(error2);
42327
- logger20.error("Failed to prepare new record for upsert", {
42485
+ logger20.error("Failed to prepare record for update", {
42328
42486
  requestId,
42329
- recordId: id,
42487
+ recordId: failedId,
42330
42488
  error: errorMessage,
42331
42489
  errorCode
42332
42490
  });
42333
- preparationFailedIds.push(id);
42334
- preparationErrors.push({
42335
- id,
42491
+ preparationFailedIds2.push(failedId);
42492
+ preparationErrors2.push({
42493
+ id: failedId,
42336
42494
  code: errorCode,
42337
42495
  message: errorMessage
42338
42496
  });
42339
42497
  }
42340
42498
  }
42341
- }
42499
+ if (upsert && notFoundIds2.length > 0) {
42500
+ const { addCreateTimestamps: addCreateTimestamps2 } = await Promise.resolve().then(() => (init_timestamps(), timestamps_exports));
42501
+ for (const id of notFoundIds2) {
42502
+ try {
42503
+ const mergedData = {
42504
+ ...applyJsonMergePatch({}, { ...update.setOnInsert, ...update.patch }),
42505
+ id
42506
+ };
42507
+ const newData = addCreateTimestamps2(mergedData);
42508
+ const newShadowRecords = generateShadowRecords(newData, resource, shadowConfig);
42509
+ const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
42510
+ const mainSK = generateMainRecordSK2(id);
42511
+ preparedRecords2.push({
42512
+ id,
42513
+ updatedData: newData,
42514
+ oldShadowKeys: [],
42515
+ // 新規作成なので空
42516
+ newShadowKeys,
42517
+ mainSK,
42518
+ isNew: true
42519
+ });
42520
+ } catch (error2) {
42521
+ const errorMessage = error2 instanceof Error ? error2.message : "Unknown preparation error";
42522
+ const errorCode = getPreparationErrorCode2(error2);
42523
+ logger20.error("Failed to prepare new record for upsert", {
42524
+ requestId,
42525
+ recordId: id,
42526
+ error: errorMessage,
42527
+ errorCode
42528
+ });
42529
+ preparationFailedIds2.push(id);
42530
+ preparationErrors2.push({
42531
+ id,
42532
+ code: errorCode,
42533
+ message: errorMessage
42534
+ });
42535
+ }
42536
+ }
42537
+ }
42538
+ return { preparedRecords: preparedRecords2, notFoundIds: notFoundIds2, preparationFailedIds: preparationFailedIds2, preparationErrors: preparationErrors2 };
42539
+ }, "prepare");
42540
+ const { preparedRecords, notFoundIds, preparationFailedIds, preparationErrors } = await prepare(ids);
42342
42541
  const getItemCount = /* @__PURE__ */ __name((record) => {
42343
42542
  const shadowDiff = calculateShadowDiff(record.oldShadowKeys, record.newShadowKeys);
42344
42543
  return 1 + shadowDiff.toDelete.length + shadowDiff.toAdd.length;
@@ -42352,7 +42551,7 @@ async function handleUpdateMany(resource, params, requestId) {
42352
42551
  chunks.length,
42353
42552
  preparationRiskInfo
42354
42553
  );
42355
- const executeChunk = /* @__PURE__ */ __name(async (chunk) => {
42554
+ const writeChunk = /* @__PURE__ */ __name(async (chunk) => {
42356
42555
  const transactItems = [];
42357
42556
  for (const record of chunk) {
42358
42557
  const shadowDiff = calculateShadowDiff(record.oldShadowKeys, record.newShadowKeys);
@@ -42363,10 +42562,13 @@ async function handleUpdateMany(resource, params, requestId) {
42363
42562
  PK: resource,
42364
42563
  SK: record.mainSK,
42365
42564
  __shadowKeys: record.newShadowKeys,
42565
+ [REVISION_ATTRIBUTE]: nextRevision(record.revision),
42366
42566
  data: {
42367
42567
  ...record.updatedData
42368
42568
  }
42369
- }
42569
+ },
42570
+ // 読んでから書くまでの間に、ほかの書き込みが入っていないこと(楽観ロック)
42571
+ ...record.isNew ? notExistsCondition() : unchangedCondition(record.revision)
42370
42572
  }
42371
42573
  });
42372
42574
  for (const shadowSK of shadowDiff.toDelete) {
@@ -42409,8 +42611,18 @@ async function handleUpdateMany(resource, params, requestId) {
42409
42611
  } else {
42410
42612
  costTracker.add(result.ConsumedCapacity);
42411
42613
  }
42412
- return chunk;
42413
- }, "executeChunk");
42614
+ }, "writeChunk");
42615
+ const executeChunk = /* @__PURE__ */ __name((chunk) => writeWithConflictRetry(chunk, writeChunk, async (stale) => {
42616
+ const fresh = await prepare(stale.map((record) => record.id));
42617
+ if (fresh.preparedRecords.length !== stale.length) {
42618
+ throw new AppError(
42619
+ "INTERNAL_ERROR" /* INTERNAL_ERROR */,
42620
+ "Records changed concurrently and could not be re-read for update",
42621
+ 409
42622
+ );
42623
+ }
42624
+ return fresh.preparedRecords;
42625
+ }), "executeChunk");
42414
42626
  const {
42415
42627
  successRecords,
42416
42628
  failedIds: chunkFailedIds,
@@ -42420,8 +42632,6 @@ async function handleUpdateMany(resource, params, requestId) {
42420
42632
  const items = [];
42421
42633
  const failedIdsMap = {};
42422
42634
  const errorsMap = {};
42423
- const actualPatchData = patchData.$set ? patchData.$set : patchData;
42424
- const setOnInsertData = patchData.$setOnInsert ? patchData.$setOnInsert : {};
42425
42635
  const successIdSet = new Set(successRecords.map((r5) => r5.id));
42426
42636
  for (let i5 = 0; i5 < ids.length; i5++) {
42427
42637
  const id = ids[i5];
@@ -42430,13 +42640,13 @@ async function handleUpdateMany(resource, params, requestId) {
42430
42640
  if (upsert && notFoundIds.includes(id)) {
42431
42641
  items.push({
42432
42642
  id,
42433
- ...setOnInsertData,
42434
- ...actualPatchData
42643
+ ...update.setOnInsert,
42644
+ ...update.returned
42435
42645
  });
42436
42646
  } else {
42437
42647
  items.push({
42438
42648
  id,
42439
- ...actualPatchData
42649
+ ...update.returned
42440
42650
  });
42441
42651
  }
42442
42652
  }
@@ -42551,7 +42761,9 @@ var init_updateMany = __esm({
42551
42761
  init_cost_tracker();
42552
42762
  init_dynamodb3();
42553
42763
  init_mergePatch();
42764
+ init_revision();
42554
42765
  init_timestamps();
42766
+ init_updateOperators();
42555
42767
  init_findAll();
42556
42768
  logger20 = createLogger({ service: "records-lambda" });
42557
42769
  __name(handleUpdateMany, "handleUpdateMany");
@@ -44605,7 +44817,7 @@ async function handler(event) {
44605
44817
  return createCorsResponse(HTTP_STATUS.OK);
44606
44818
  }
44607
44819
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
44608
- const version = "1.24.1";
44820
+ const version = "1.25.1";
44609
44821
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
44610
44822
  }
44611
44823
  if (event.requestContext.http.method !== "POST") {