@exabugs/dynamodb-client 1.27.0 → 1.27.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.
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.27.0
2
- // Built: 2026-11-15T08:17:35.832Z
1
+ // @exabugs/dynamodb-client v1.27.1
2
+ // Built: 2026-11-15T11:40:11.703Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -38934,6 +38934,9 @@ function isGeoCoordinates(value) {
38934
38934
  function encodeGeoHash(latitude, longitude, precision = 6) {
38935
38935
  return import_ngeohash.default.encode(latitude, longitude, precision);
38936
38936
  }
38937
+ function decodeGeoHash(geohash) {
38938
+ return import_ngeohash.default.decode(geohash);
38939
+ }
38937
38940
  function getNeighborGeoHashes(geohash) {
38938
38941
  const neighbors = [
38939
38942
  geohash,
@@ -39004,6 +39007,7 @@ var init_utils5 = __esm({
39004
39007
  import_ngeohash = __toESM(require_main(), 1);
39005
39008
  __name(isGeoCoordinates, "isGeoCoordinates");
39006
39009
  __name(encodeGeoHash, "encodeGeoHash");
39010
+ __name(decodeGeoHash, "decodeGeoHash");
39007
39011
  __name(getNeighborGeoHashes, "getNeighborGeoHashes");
39008
39012
  __name(calculateDistance, "calculateDistance");
39009
39013
  __name(isNearQueryGeoJSON, "isNearQueryGeoJSON");
@@ -39808,6 +39812,37 @@ var init_utils6 = __esm({
39808
39812
  }
39809
39813
  });
39810
39814
 
39815
+ // src/server/utils/idFilter.ts
39816
+ function splitIdFilter(filter) {
39817
+ const { id, ...rest } = filter ?? {};
39818
+ if (typeof id === "string") {
39819
+ return { id, rest };
39820
+ }
39821
+ if (typeof id === "object" && id !== null && "$in" in id) {
39822
+ return { ids: (id.$in ?? []).slice(), rest };
39823
+ }
39824
+ return { rest: filter ?? {} };
39825
+ }
39826
+ function conditionOf(rest) {
39827
+ return Object.keys(rest).length > 0 ? rest : void 0;
39828
+ }
39829
+ function conditionMatcher(condition) {
39830
+ if (!condition || Object.keys(condition).length === 0) {
39831
+ return () => true;
39832
+ }
39833
+ const parsed = parseFilters(condition);
39834
+ return (record) => matchesAllFilters(record, parsed);
39835
+ }
39836
+ var init_idFilter = __esm({
39837
+ "src/server/utils/idFilter.ts"() {
39838
+ "use strict";
39839
+ init_utils6();
39840
+ __name(splitIdFilter, "splitIdFilter");
39841
+ __name(conditionOf, "conditionOf");
39842
+ __name(conditionMatcher, "conditionMatcher");
39843
+ }
39844
+ });
39845
+
39811
39846
  // src/server/utils/batchGet.ts
39812
39847
  function sleep2(ms) {
39813
39848
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -39929,80 +39964,68 @@ var init_converter = __esm({
39929
39964
  });
39930
39965
 
39931
39966
  // src/server/query/nearSearch.ts
39932
- function coversMaxDistance(precision, maxDistance) {
39933
- const coverage = GEOHASH_COVERAGE[precision];
39934
- if (!coverage) {
39935
- return false;
39936
- }
39937
- return coverage >= maxDistance;
39967
+ function cellSizeMeters(precision, cosLat) {
39968
+ const latBits = Math.floor(5 * precision / 2);
39969
+ const lonBits = Math.ceil(5 * precision / 2);
39970
+ const heightDeg = 180 / 2 ** latBits;
39971
+ return {
39972
+ height: heightDeg * METERS_PER_DEGREE,
39973
+ width: 360 / 2 ** lonBits * METERS_PER_DEGREE * cosLat,
39974
+ heightDeg
39975
+ };
39976
+ }
39977
+ function guaranteedRadius(precision, latitude) {
39978
+ const { heightDeg } = cellSizeMeters(precision, 1);
39979
+ const worstLatitude = Math.min(89.9, Math.abs(latitude) + 2 * heightDeg);
39980
+ const { height, width } = cellSizeMeters(precision, Math.cos(worstLatitude * Math.PI / 180));
39981
+ return Math.min(height, width);
39982
+ }
39983
+ function shadowPositionError(shadowPrecision) {
39984
+ const { height, width } = cellSizeMeters(shadowPrecision, 1);
39985
+ return Math.hypot(height, width) / 2;
39938
39986
  }
39939
- async function executeNearSearch(nearQuery, fieldName, limit, searchFunction, config = DEFAULT_GEOHASH_CONFIG) {
39987
+ function kthOf(sortedAscending, k5) {
39988
+ return sortedAscending.length >= k5 ? sortedAscending[k5 - 1] : void 0;
39989
+ }
39990
+ async function executeNearSearch(nearQuery, fieldName, limit, searchShadows, readBodies, config = DEFAULT_GEOHASH_CONFIG) {
39940
39991
  const { latitude, longitude } = extractCoordinatesFromNearQuery(nearQuery);
39941
39992
  const maxDistance = extractMaxDistanceFromNearQuery(nearQuery);
39942
39993
  const minDistance = extractMinDistanceFromNearQuery(nearQuery);
39994
+ const epsilon = shadowPositionError(config.shadowPrecision);
39943
39995
  let precision = config.searchPrecision;
39944
- let allCandidates = [];
39945
39996
  let iterations = 0;
39946
- while (allCandidates.length < limit * config.candidateMultiplier && precision >= config.minPrecision && iterations < config.maxIterations) {
39997
+ let exhaustive = false;
39998
+ let approx = /* @__PURE__ */ new Map();
39999
+ let kth;
40000
+ while (precision >= config.minPrecision && iterations < config.maxIterations) {
39947
40001
  iterations++;
39948
- const currentGeoHash = encodeGeoHash(latitude, longitude, precision);
39949
- const neighborGeoHashes = getNeighborGeoHashes(currentGeoHash);
39950
- const searchPromises = neighborGeoHashes.map((geohash) => searchFunction(geohash));
39951
- const results = await Promise.all(searchPromises);
39952
- const candidates = results.flat();
39953
- for (const candidate of candidates) {
39954
- if (!allCandidates.some((c5) => c5.id === candidate.id)) {
39955
- allCandidates.push(candidate);
39956
- }
39957
- }
39958
- if (maxDistance !== void 0) {
39959
- let candidatesWithinDistance = 0;
39960
- for (const candidate of allCandidates) {
39961
- const location = candidate[fieldName];
39962
- if (location && typeof location.latitude === "number" && typeof location.longitude === "number") {
39963
- const distance = calculateDistance(
39964
- latitude,
39965
- longitude,
39966
- location.latitude,
39967
- location.longitude
39968
- );
39969
- if (distance <= maxDistance) {
39970
- candidatesWithinDistance++;
39971
- }
39972
- }
39973
- }
39974
- if (candidatesWithinDistance >= limit) {
39975
- console.log("[nearSearch] Early termination: found enough candidates within maxDistance", {
39976
- candidatesWithinDistance,
39977
- limit,
39978
- maxDistance,
39979
- precision,
39980
- iterations
39981
- });
39982
- break;
39983
- }
39984
- if (coversMaxDistance(precision, maxDistance)) {
39985
- console.log("[nearSearch] Early termination: current precision covers maxDistance", {
39986
- precision,
39987
- maxDistance,
39988
- coverage: GEOHASH_COVERAGE[precision],
39989
- candidatesWithinDistance,
39990
- iterations
39991
- });
39992
- break;
39993
- }
40002
+ const radius = guaranteedRadius(precision, latitude);
40003
+ const neighbors = getNeighborGeoHashes(encodeGeoHash(latitude, longitude, precision));
40004
+ const hits = (await Promise.all(neighbors.map((prefix) => searchShadows(prefix)))).flat();
40005
+ approx = /* @__PURE__ */ new Map();
40006
+ for (const hit of hits) {
40007
+ const center = decodeGeoHash(hit.geohash);
40008
+ approx.set(hit.id, calculateDistance(latitude, longitude, center.latitude, center.longitude));
40009
+ }
40010
+ const sure = [...approx.values()].filter(
40011
+ (d5) => (minDistance === void 0 || d5 - epsilon >= minDistance) && (maxDistance === void 0 || d5 + epsilon <= maxDistance)
40012
+ ).sort((a5, b5) => a5 - b5);
40013
+ kth = kthOf(sure, limit);
40014
+ const topKConfirmed = kth !== void 0 && kth + epsilon <= radius;
40015
+ const circleCovered = maxDistance !== void 0 && maxDistance + epsilon <= radius;
40016
+ if (topKConfirmed || circleCovered) {
40017
+ exhaustive = true;
40018
+ break;
39994
40019
  }
39995
40020
  precision--;
39996
40021
  }
39997
- const documentsWithDistance = allCandidates.map((doc) => {
40022
+ const cutoff = kth !== void 0 ? kth + 2 * epsilon : void 0;
40023
+ const wanted = [...approx.entries()].filter(
40024
+ ([, d5]) => (cutoff === void 0 || d5 <= cutoff) && (maxDistance === void 0 || d5 <= maxDistance + epsilon) && (minDistance === void 0 || d5 >= minDistance - epsilon)
40025
+ ).map(([id]) => id);
40026
+ const bodies = wanted.length > 0 ? await readBodies(wanted) : [];
40027
+ const documents = bodies.map((doc) => {
39998
40028
  const location = doc[fieldName];
39999
- if (!location) {
40000
- console.log("[nearSearch] Location field not found:", {
40001
- fieldName,
40002
- docKeys: Object.keys(doc),
40003
- doc: JSON.stringify(doc).substring(0, 200)
40004
- });
40005
- }
40006
40029
  if (!location || typeof location.latitude !== "number" || typeof location.longitude !== "number") {
40007
40030
  return null;
40008
40031
  }
@@ -40012,19 +40035,8 @@ async function executeNearSearch(nearQuery, fieldName, limit, searchFunction, co
40012
40035
  location.latitude,
40013
40036
  location.longitude
40014
40037
  );
40015
- console.log("[nearSearch] Distance calculated:", {
40016
- docId: doc.id,
40017
- distance,
40018
- maxDistance,
40019
- minDistance,
40020
- willBeFiltered: maxDistance !== void 0 && distance > maxDistance || minDistance !== void 0 && distance < minDistance
40021
- });
40022
- if (maxDistance !== void 0 && distance > maxDistance) {
40023
- return null;
40024
- }
40025
- if (minDistance !== void 0 && distance < minDistance) {
40026
- return null;
40027
- }
40038
+ if (maxDistance !== void 0 && distance > maxDistance) return null;
40039
+ if (minDistance !== void 0 && distance < minDistance) return null;
40028
40040
  return {
40029
40041
  ...doc,
40030
40042
  __distance: distance,
@@ -40032,36 +40044,26 @@ async function executeNearSearch(nearQuery, fieldName, limit, searchFunction, co
40032
40044
  };
40033
40045
  }).filter((doc) => doc !== null).sort((a5, b5) => (a5.__distance ?? 0) - (b5.__distance ?? 0)).slice(0, limit);
40034
40046
  return {
40035
- documents: documentsWithDistance,
40047
+ documents,
40036
40048
  metadata: {
40037
40049
  iterations,
40038
- candidatesFound: allCandidates.length,
40039
- requestedLimit: limit
40050
+ candidatesFound: approx.size,
40051
+ bodiesRead: wanted.length,
40052
+ requestedLimit: limit,
40053
+ exhaustive
40040
40054
  }
40041
40055
  };
40042
40056
  }
40043
- var GEOHASH_COVERAGE;
40057
+ var METERS_PER_DEGREE;
40044
40058
  var init_nearSearch = __esm({
40045
40059
  "src/server/query/nearSearch.ts"() {
40046
40060
  "use strict";
40047
40061
  init_geohash();
40048
- GEOHASH_COVERAGE = {
40049
- 8: 19 * 3,
40050
- // ±19m × 3 = 約57m
40051
- 7: 76 * 3,
40052
- // ±76m × 3 = 約228m
40053
- 6: 610 * 3,
40054
- // ±610m × 3 = 約1,830m (1.8km)
40055
- 5: 2400 * 3,
40056
- // ±2.4km × 3 = 約7,200m (7.2km)
40057
- 4: 2e4 * 3,
40058
- // ±20km × 3 = 約60,000m (60km)
40059
- 3: 78e3 * 3,
40060
- // ±78km × 3 = 約234,000m (234km)
40061
- 2: 63e4 * 3
40062
- // ±630km × 3 = 約1,890,000m (1,890km)
40063
- };
40064
- __name(coversMaxDistance, "coversMaxDistance");
40062
+ METERS_PER_DEGREE = 111320;
40063
+ __name(cellSizeMeters, "cellSizeMeters");
40064
+ __name(guaranteedRadius, "guaranteedRadius");
40065
+ __name(shadowPositionError, "shadowPositionError");
40066
+ __name(kthOf, "kthOf");
40065
40067
  __name(executeNearSearch, "executeNearSearch");
40066
40068
  }
40067
40069
  });
@@ -40543,149 +40545,6 @@ var init_idQuery = __esm({
40543
40545
  }
40544
40546
  });
40545
40547
 
40546
- // src/server/operations/find/nearQuery.ts
40547
- async function executeNearQuery(resource, fieldName, nearQuery, limit, requestId) {
40548
- logger7.debug("Executing $near query", {
40549
- requestId,
40550
- resource,
40551
- fieldName,
40552
- nearQuery,
40553
- limit
40554
- });
40555
- const costTracker = new CostTracker();
40556
- const searchFunction = /* @__PURE__ */ __name(async (geohashPrefix) => {
40557
- const dbClient2 = getDBClient();
40558
- const tableName = getTableName();
40559
- logger7.debug("Searching shadow records", {
40560
- requestId,
40561
- resource,
40562
- fieldName,
40563
- geohashPrefix,
40564
- skPrefix: `${fieldName}#${geohashPrefix}`
40565
- });
40566
- const shadowRecords = [];
40567
- const watcher = new ScanWatcher({
40568
- requestId,
40569
- resource,
40570
- path: "near-search",
40571
- sortField: fieldName,
40572
- filters: ["$near"]
40573
- });
40574
- let exclusiveStartKey;
40575
- let previousKeySignature;
40576
- do {
40577
- const queryResult = await executeDynamoDBOperation(
40578
- () => dbClient2.send(
40579
- new import_lib_dynamodb4.QueryCommand({
40580
- TableName: tableName,
40581
- KeyConditionExpression: "PK = :pk AND begins_with(SK, :skPrefix)",
40582
- ExpressionAttributeValues: {
40583
- ":pk": resource,
40584
- ":skPrefix": `${fieldName}#${geohashPrefix}`
40585
- },
40586
- ExclusiveStartKey: exclusiveStartKey,
40587
- ConsistentRead: false,
40588
- // シャドウレコードは結果整合性で十分
40589
- ReturnConsumedCapacity: "TOTAL"
40590
- })
40591
- ),
40592
- "Query"
40593
- );
40594
- costTracker.add(queryResult.ConsumedCapacity);
40595
- shadowRecords.push(...queryResult.Items || []);
40596
- watcher.add((queryResult.Items || []).length);
40597
- const nextKey = queryResult.LastEvaluatedKey;
40598
- const nextKeySignature = nextKey ? `${nextKey.PK}#${nextKey.SK}` : void 0;
40599
- if (nextKeySignature && nextKeySignature === previousKeySignature) {
40600
- throw new AppError(
40601
- "INTERNAL_ERROR" /* INTERNAL_ERROR */,
40602
- "Near search received the same LastEvaluatedKey repeatedly",
40603
- 500,
40604
- { resource, geohashPrefix }
40605
- );
40606
- }
40607
- previousKeySignature = nextKeySignature;
40608
- exclusiveStartKey = nextKey;
40609
- } while (exclusiveStartKey);
40610
- logger7.debug("Shadow records found", {
40611
- requestId,
40612
- resource,
40613
- geohashPrefix,
40614
- count: shadowRecords.length
40615
- });
40616
- const mainRecordIds = shadowRecords.map((item) => {
40617
- const sk = item.SK;
40618
- const parts = sk.split("#id#");
40619
- return parts.length === 2 ? parts[1] : null;
40620
- }).filter((id) => id !== null);
40621
- logger7.debug("Main record IDs extracted", {
40622
- requestId,
40623
- resource,
40624
- geohashPrefix,
40625
- ids: mainRecordIds
40626
- });
40627
- const validRecords = await batchGetAll(
40628
- mainRecordIds.map((id) => ({ PK: resource, SK: `id#${id}` })),
40629
- { consistentRead: false, costTracker, requestId }
40630
- );
40631
- logger7.debug("Main records retrieved", {
40632
- requestId,
40633
- resource,
40634
- geohashPrefix,
40635
- count: validRecords.length
40636
- });
40637
- const cleanRecords = validRecords.map((record) => extractCleanRecord(record));
40638
- logger7.debug("Clean records extracted", {
40639
- requestId,
40640
- resource,
40641
- geohashPrefix,
40642
- count: cleanRecords.length,
40643
- sampleKeys: cleanRecords[0] ? Object.keys(cleanRecords[0]) : []
40644
- });
40645
- return cleanRecords;
40646
- }, "searchFunction");
40647
- const minPrecisionOverride = "minPrecision" in nearQuery ? nearQuery.minPrecision : "$minPrecision" in nearQuery ? nearQuery.$minPrecision : void 0;
40648
- const config = minPrecisionOverride !== void 0 ? { ...DEFAULT_GEOHASH_CONFIG, minPrecision: minPrecisionOverride } : DEFAULT_GEOHASH_CONFIG;
40649
- const result = await executeNearSearch(nearQuery, fieldName, limit, searchFunction, config);
40650
- const items = result.documents;
40651
- logger7.info("$near query succeeded", {
40652
- requestId,
40653
- resource,
40654
- fieldName,
40655
- count: items.length,
40656
- iterations: result.metadata.iterations,
40657
- candidatesFound: result.metadata.candidatesFound
40658
- });
40659
- return {
40660
- items,
40661
- pageInfo: {
40662
- hasNextPage: false,
40663
- // $near検索はページネーション非対応
40664
- hasPreviousPage: false
40665
- },
40666
- consumedCapacity: costTracker.getAggregated()
40667
- };
40668
- }
40669
- var import_lib_dynamodb4, logger7;
40670
- var init_nearQuery = __esm({
40671
- "src/server/operations/find/nearQuery.ts"() {
40672
- "use strict";
40673
- import_lib_dynamodb4 = __toESM(require_dist_cjs22(), 1);
40674
- init_geohash();
40675
- init_shared();
40676
- init_nearSearch();
40677
- init_batchGet();
40678
- init_cost_tracker();
40679
- init_dynamodb3();
40680
- init_scanWarning();
40681
- logger7 = createLogger({
40682
- service: "near-query",
40683
- level: process.env.LOG_LEVEL || "info"
40684
- });
40685
- __name(executeNearQuery, "executeNearQuery");
40686
- }
40687
- });
40688
-
40689
40548
  // src/server/operations/find/shadowQuery.ts
40690
40549
  async function executeShadowQuery(resource, normalizedParams, requestId) {
40691
40550
  const { sort, pagination, parsedFilters } = normalizedParams;
@@ -40714,7 +40573,7 @@ async function executeShadowQuery(resource, normalizedParams, requestId) {
40714
40573
  (f5) => !(isKeyConditionOptimized && (f5 === optimizableFilter || f5 === upperFilter))
40715
40574
  );
40716
40575
  if (residualFilters.length > 0) {
40717
- logger8.debug("Residual filters detected, falling back to full scan", {
40576
+ logger7.debug("Residual filters detected, falling back to full scan", {
40718
40577
  requestId,
40719
40578
  resource,
40720
40579
  sortField: sort.field,
@@ -40722,7 +40581,7 @@ async function executeShadowQuery(resource, normalizedParams, requestId) {
40722
40581
  });
40723
40582
  return executeFullScanQuery(resource, normalizedParams, costTracker, requestId);
40724
40583
  }
40725
- logger8.debug("Executing shadow query", {
40584
+ logger7.debug("Executing shadow query", {
40726
40585
  requestId,
40727
40586
  resource,
40728
40587
  sortField: sort.field,
@@ -40771,7 +40630,7 @@ async function executeShadowQuery(resource, normalizedParams, requestId) {
40771
40630
  shadowRecords.LastEvaluatedKey.PK,
40772
40631
  shadowRecords.LastEvaluatedKey.SK
40773
40632
  ) : void 0;
40774
- logger8.info("Shadow query succeeded", {
40633
+ logger7.info("Shadow query succeeded", {
40775
40634
  requestId,
40776
40635
  resource,
40777
40636
  sortField: sort.field,
@@ -40796,7 +40655,7 @@ async function executeFilterFirstQuery(resource, normalizedParams, filterCandida
40796
40655
  const offset = nextToken ? decodeOffsetToken(nextToken) ?? 0 : 0;
40797
40656
  const filterSort = { field: filterCandidate.parsed.field, order: "ASC" };
40798
40657
  const remainingFilters = parsedFilters;
40799
- logger8.debug("Executing filter-first query", {
40658
+ logger7.debug("Executing filter-first query", {
40800
40659
  requestId,
40801
40660
  resource,
40802
40661
  filterField: filterCandidate.parsed.field,
@@ -40819,7 +40678,7 @@ async function executeFilterFirstQuery(resource, normalizedParams, filterCandida
40819
40678
  const page = items.slice(offset, offset + perPage);
40820
40679
  const hasNextPage = offset + perPage < items.length;
40821
40680
  const nextTokenValue = hasNextPage ? encodeOffsetToken(offset + perPage) : void 0;
40822
- logger8.info("Filter-first query succeeded", {
40681
+ logger7.info("Filter-first query succeeded", {
40823
40682
  requestId,
40824
40683
  resource,
40825
40684
  filterField: filterCandidate.parsed.field,
@@ -40840,7 +40699,7 @@ async function executeFullScanQuery(resource, normalizedParams, costTracker, req
40840
40699
  const { sort, pagination, parsedFilters } = normalizedParams;
40841
40700
  const { perPage, nextToken } = pagination;
40842
40701
  const offset = nextToken ? decodeOffsetToken(nextToken) ?? 0 : 0;
40843
- logger8.debug("Executing full scan query", {
40702
+ logger7.debug("Executing full scan query", {
40844
40703
  requestId,
40845
40704
  resource,
40846
40705
  sortField: sort.field,
@@ -40861,7 +40720,7 @@ async function executeFullScanQuery(resource, normalizedParams, costTracker, req
40861
40720
  const page = items.slice(offset, offset + perPage);
40862
40721
  const hasNextPage = offset + perPage < items.length;
40863
40722
  const nextTokenValue = hasNextPage ? encodeOffsetToken(offset + perPage) : void 0;
40864
- logger8.info("Full scan query succeeded", {
40723
+ logger7.info("Full scan query succeeded", {
40865
40724
  requestId,
40866
40725
  resource,
40867
40726
  sortField: sort.field,
@@ -40892,7 +40751,7 @@ async function fetchAllRecordsViaShadowIndex(resource, indexSort, indexFilter, c
40892
40751
  let iterations = 0;
40893
40752
  do {
40894
40753
  if (++iterations > MAX_ITERATIONS) {
40895
- logger8.error("fetchAllRecordsViaShadowIndex: \u6700\u5927\u53CD\u5FA9\u56DE\u6570\u3092\u8D85\u904E", {
40754
+ logger7.error("fetchAllRecordsViaShadowIndex: \u6700\u5927\u53CD\u5FA9\u56DE\u6570\u3092\u8D85\u904E", {
40896
40755
  requestId,
40897
40756
  resource,
40898
40757
  iterations
@@ -40922,7 +40781,7 @@ async function fetchAllRecordsViaShadowIndex(resource, indexSort, indexFilter, c
40922
40781
  const nextKey = result.LastEvaluatedKey;
40923
40782
  const nextKeySignature = nextKey ? `${nextKey.PK}#${nextKey.SK}` : void 0;
40924
40783
  if (nextKeySignature && nextKeySignature === previousKeySignature) {
40925
- logger8.error("fetchAllRecordsViaShadowIndex: \u540C\u4E00\u306ELastEvaluatedKey\u304C\u8FD4\u5374\u3055\u308C\u305F", {
40784
+ logger7.error("fetchAllRecordsViaShadowIndex: \u540C\u4E00\u306ELastEvaluatedKey\u304C\u8FD4\u5374\u3055\u308C\u305F", {
40926
40785
  requestId,
40927
40786
  resource,
40928
40787
  iterations
@@ -40995,7 +40854,7 @@ async function executeShadowRecordQuery(resource, sort, perPage, nextToken, opti
40995
40854
  );
40996
40855
  const result = await executeDynamoDBOperation(
40997
40856
  () => dbClient2.send(
40998
- new import_lib_dynamodb5.QueryCommand({
40857
+ new import_lib_dynamodb4.QueryCommand({
40999
40858
  TableName: tableName,
41000
40859
  KeyConditionExpression: keyConditionExpression,
41001
40860
  ExpressionAttributeValues: expressionAttributeValues,
@@ -41023,7 +40882,7 @@ async function fetchMainRecords(resource, recordIds, costTracker, requestId) {
41023
40882
  recordIds.map((id) => ({ PK: resource, SK: `id#${id}` })),
41024
40883
  { consistentRead: true, costTracker, requestId }
41025
40884
  );
41026
- logger8.debug("Main records fetched", {
40885
+ logger7.debug("Main records fetched", {
41027
40886
  requestId,
41028
40887
  resource,
41029
40888
  requestedCount: new Set(recordIds).size,
@@ -41031,11 +40890,11 @@ async function fetchMainRecords(resource, recordIds, costTracker, requestId) {
41031
40890
  });
41032
40891
  return allRecords;
41033
40892
  }
41034
- var import_lib_dynamodb5, logger8;
40893
+ var import_lib_dynamodb4, logger7;
41035
40894
  var init_shadowQuery = __esm({
41036
40895
  "src/server/operations/find/shadowQuery.ts"() {
41037
40896
  "use strict";
41038
- import_lib_dynamodb5 = __toESM(require_dist_cjs22(), 1);
40897
+ import_lib_dynamodb4 = __toESM(require_dist_cjs22(), 1);
41039
40898
  init_shared();
41040
40899
  init_batchGet();
41041
40900
  init_cost_tracker();
@@ -41044,7 +40903,7 @@ var init_shadowQuery = __esm({
41044
40903
  init_scanWarning();
41045
40904
  init_keyCondition();
41046
40905
  init_utils6();
41047
- logger8 = createLogger({
40906
+ logger7 = createLogger({
41048
40907
  service: "shadow-query",
41049
40908
  level: process.env.LOG_LEVEL || "info"
41050
40909
  });
@@ -41060,6 +40919,148 @@ var init_shadowQuery = __esm({
41060
40919
  }
41061
40920
  });
41062
40921
 
40922
+ // src/server/operations/find/nearQuery.ts
40923
+ async function executeNearQuery(resource, fieldName, nearQuery, limit, requestId, condition) {
40924
+ logger8.debug("Executing $near query", {
40925
+ requestId,
40926
+ resource,
40927
+ fieldName,
40928
+ nearQuery,
40929
+ limit
40930
+ });
40931
+ const costTracker = new CostTracker();
40932
+ const searchShadows = /* @__PURE__ */ __name(async (geohashPrefix) => {
40933
+ const dbClient2 = getDBClient();
40934
+ const tableName = getTableName();
40935
+ logger8.debug("Searching shadow records", {
40936
+ requestId,
40937
+ resource,
40938
+ fieldName,
40939
+ geohashPrefix,
40940
+ skPrefix: `${fieldName}#${geohashPrefix}`
40941
+ });
40942
+ const shadowRecords = [];
40943
+ const watcher = new ScanWatcher({
40944
+ requestId,
40945
+ resource,
40946
+ path: "near-search",
40947
+ sortField: fieldName,
40948
+ filters: ["$near"]
40949
+ });
40950
+ let exclusiveStartKey;
40951
+ let previousKeySignature;
40952
+ do {
40953
+ const queryResult = await executeDynamoDBOperation(
40954
+ () => dbClient2.send(
40955
+ new import_lib_dynamodb5.QueryCommand({
40956
+ TableName: tableName,
40957
+ KeyConditionExpression: "PK = :pk AND begins_with(SK, :skPrefix)",
40958
+ ExpressionAttributeValues: {
40959
+ ":pk": resource,
40960
+ ":skPrefix": `${fieldName}#${geohashPrefix}`
40961
+ },
40962
+ ExclusiveStartKey: exclusiveStartKey,
40963
+ ConsistentRead: false,
40964
+ // シャドウレコードは結果整合性で十分
40965
+ ReturnConsumedCapacity: "TOTAL"
40966
+ })
40967
+ ),
40968
+ "Query"
40969
+ );
40970
+ costTracker.add(queryResult.ConsumedCapacity);
40971
+ shadowRecords.push(...queryResult.Items || []);
40972
+ watcher.add((queryResult.Items || []).length);
40973
+ const nextKey = queryResult.LastEvaluatedKey;
40974
+ const nextKeySignature = nextKey ? `${nextKey.PK}#${nextKey.SK}` : void 0;
40975
+ if (nextKeySignature && nextKeySignature === previousKeySignature) {
40976
+ throw new AppError(
40977
+ "INTERNAL_ERROR" /* INTERNAL_ERROR */,
40978
+ "Near search received the same LastEvaluatedKey repeatedly",
40979
+ 500,
40980
+ { resource, geohashPrefix }
40981
+ );
40982
+ }
40983
+ previousKeySignature = nextKeySignature;
40984
+ exclusiveStartKey = nextKey;
40985
+ } while (exclusiveStartKey);
40986
+ logger8.debug("Shadow records found", {
40987
+ requestId,
40988
+ resource,
40989
+ geohashPrefix,
40990
+ count: shadowRecords.length
40991
+ });
40992
+ const hits = [];
40993
+ for (const record of shadowRecords) {
40994
+ const sk = String(record.SK);
40995
+ const afterField = sk.slice(fieldName.length + 1);
40996
+ const geohash = afterField.slice(0, afterField.indexOf("#"));
40997
+ const [id] = extractRecordIds([record]);
40998
+ if (id !== void 0 && geohash !== "") hits.push({ id, geohash });
40999
+ }
41000
+ return hits;
41001
+ }, "searchShadows");
41002
+ const matches = conditionMatcher(condition);
41003
+ const readBodies = /* @__PURE__ */ __name(async (ids) => {
41004
+ const bodies = await batchGetAll(
41005
+ ids.map((id) => ({ PK: resource, SK: `id#${id}` })),
41006
+ { consistentRead: false, costTracker, requestId }
41007
+ );
41008
+ return bodies.map((record) => extractCleanRecord(record)).filter(matches);
41009
+ }, "readBodies");
41010
+ const minPrecisionOverride = "minPrecision" in nearQuery ? nearQuery.minPrecision : "$minPrecision" in nearQuery ? nearQuery.$minPrecision : void 0;
41011
+ const config = minPrecisionOverride !== void 0 ? { ...DEFAULT_GEOHASH_CONFIG, minPrecision: minPrecisionOverride } : DEFAULT_GEOHASH_CONFIG;
41012
+ const result = await executeNearSearch(
41013
+ nearQuery,
41014
+ fieldName,
41015
+ limit,
41016
+ searchShadows,
41017
+ readBodies,
41018
+ config
41019
+ );
41020
+ const items = result.documents;
41021
+ logger8.info("$near query succeeded", {
41022
+ requestId,
41023
+ resource,
41024
+ fieldName,
41025
+ count: items.length,
41026
+ iterations: result.metadata.iterations,
41027
+ candidatesFound: result.metadata.candidatesFound,
41028
+ bodiesRead: result.metadata.bodiesRead,
41029
+ exhaustive: result.metadata.exhaustive
41030
+ });
41031
+ return {
41032
+ items,
41033
+ pageInfo: {
41034
+ hasNextPage: false,
41035
+ // $near検索はページネーション非対応
41036
+ hasPreviousPage: false,
41037
+ exhaustive: result.metadata.exhaustive
41038
+ },
41039
+ consumedCapacity: costTracker.getAggregated()
41040
+ };
41041
+ }
41042
+ var import_lib_dynamodb5, logger8;
41043
+ var init_nearQuery = __esm({
41044
+ "src/server/operations/find/nearQuery.ts"() {
41045
+ "use strict";
41046
+ import_lib_dynamodb5 = __toESM(require_dist_cjs22(), 1);
41047
+ init_geohash();
41048
+ init_shared();
41049
+ init_nearSearch();
41050
+ init_batchGet();
41051
+ init_cost_tracker();
41052
+ init_dynamodb3();
41053
+ init_idFilter();
41054
+ init_scanWarning();
41055
+ init_shadowQuery();
41056
+ logger8 = createLogger({
41057
+ service: "near-query",
41058
+ level: process.env.LOG_LEVEL || "info"
41059
+ });
41060
+ __name(executeNearQuery, "executeNearQuery");
41061
+ }
41062
+ });
41063
+
41063
41064
  // src/server/operations/find/handler.ts
41064
41065
  async function handleFind(resource, params, requestId) {
41065
41066
  logger9.debug("Executing find", {
@@ -41075,12 +41076,16 @@ async function handleFind(resource, params, requestId) {
41075
41076
  fieldName: nearDetection.fieldName
41076
41077
  });
41077
41078
  const limit = params.pagination?.perPage || 10;
41079
+ const otherFilters = Object.fromEntries(
41080
+ Object.entries(params.filter ?? {}).filter(([field]) => field !== nearDetection.fieldName)
41081
+ );
41078
41082
  return executeNearQuery(
41079
41083
  resource,
41080
41084
  nearDetection.fieldName,
41081
41085
  nearDetection.nearQuery,
41082
41086
  limit,
41083
- requestId
41087
+ requestId,
41088
+ conditionOf(otherFilters)
41084
41089
  );
41085
41090
  }
41086
41091
  const config = initializeFindConfig();
@@ -41106,6 +41111,7 @@ var init_handler = __esm({
41106
41111
  "src/server/operations/find/handler.ts"() {
41107
41112
  "use strict";
41108
41113
  init_shared();
41114
+ init_idFilter();
41109
41115
  init_validation2();
41110
41116
  init_idQuery();
41111
41117
  init_nearQuery();
@@ -41422,37 +41428,6 @@ var init_chunking = __esm({
41422
41428
  }
41423
41429
  });
41424
41430
 
41425
- // src/server/utils/idFilter.ts
41426
- function splitIdFilter(filter) {
41427
- const { id, ...rest } = filter ?? {};
41428
- if (typeof id === "string") {
41429
- return { id, rest };
41430
- }
41431
- if (typeof id === "object" && id !== null && "$in" in id) {
41432
- return { ids: (id.$in ?? []).slice(), rest };
41433
- }
41434
- return { rest: filter ?? {} };
41435
- }
41436
- function conditionOf(rest) {
41437
- return Object.keys(rest).length > 0 ? rest : void 0;
41438
- }
41439
- function conditionMatcher(condition) {
41440
- if (!condition || Object.keys(condition).length === 0) {
41441
- return () => true;
41442
- }
41443
- const parsed = parseFilters(condition);
41444
- return (record) => matchesAllFilters(record, parsed);
41445
- }
41446
- var init_idFilter = __esm({
41447
- "src/server/utils/idFilter.ts"() {
41448
- "use strict";
41449
- init_utils6();
41450
- __name(splitIdFilter, "splitIdFilter");
41451
- __name(conditionOf, "conditionOf");
41452
- __name(conditionMatcher, "conditionMatcher");
41453
- }
41454
- });
41455
-
41456
41431
  // src/server/utils/revision.ts
41457
41432
  function revisionOf(item) {
41458
41433
  const rev = item[REVISION_ATTRIBUTE];
@@ -42362,10 +42337,39 @@ function unsetFields(value) {
42362
42337
  }
42363
42338
  return Object.keys(asObject(value, "$unset"));
42364
42339
  }
42340
+ function incFields(value) {
42341
+ const inc = {};
42342
+ for (const [field, amount] of Object.entries(asObject(value, "$inc"))) {
42343
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
42344
+ throw invalid(`$inc value for "${field}" must be a finite number`);
42345
+ }
42346
+ inc[field] = amount;
42347
+ }
42348
+ return inc;
42349
+ }
42350
+ function readByPath(data, path) {
42351
+ let current = data;
42352
+ for (const key of path.split(".")) {
42353
+ if (typeof current !== "object" || current === null) return void 0;
42354
+ current = current[key];
42355
+ }
42356
+ return current;
42357
+ }
42358
+ function applyInc(data, inc) {
42359
+ const added = {};
42360
+ for (const [field, amount] of Object.entries(inc)) {
42361
+ const current = readByPath(data, field);
42362
+ if (current !== void 0 && current !== null && (typeof current !== "number" || !Number.isFinite(current))) {
42363
+ throw invalid(`Field "${field}" is not a number, $inc cannot be applied`);
42364
+ }
42365
+ added[field] = (current ?? 0) + amount;
42366
+ }
42367
+ return added;
42368
+ }
42365
42369
  function resolveUpdateOperators(data) {
42366
42370
  const operators = Object.keys(data).filter((key) => key.startsWith("$"));
42367
42371
  if (operators.length === 0) {
42368
- return { patch: data, setOnInsert: {}, returned: data };
42372
+ return { patch: data, setOnInsert: {}, inc: {}, returned: data };
42369
42373
  }
42370
42374
  const unsupported = operators.filter((op2) => !SUPPORTED_OPERATORS.includes(op2));
42371
42375
  if (unsupported.length > 0) {
@@ -42381,7 +42385,12 @@ function resolveUpdateOperators(data) {
42381
42385
  }
42382
42386
  patch[field] = null;
42383
42387
  }
42384
- return { patch, setOnInsert: asObject(data.$setOnInsert, "$setOnInsert"), returned: set };
42388
+ const inc = incFields(data.$inc);
42389
+ for (const field of Object.keys(inc)) {
42390
+ if (field in set) throw invalid(`Field "${field}" is in both $set and $inc`);
42391
+ if (field in patch) throw invalid(`Field "${field}" is in both $unset and $inc`);
42392
+ }
42393
+ return { patch, setOnInsert: asObject(data.$setOnInsert, "$setOnInsert"), inc, returned: set };
42385
42394
  }
42386
42395
  function equalityFieldsOf(filter) {
42387
42396
  const fields = {};
@@ -42397,10 +42406,13 @@ var init_updateOperators = __esm({
42397
42406
  "src/server/utils/updateOperators.ts"() {
42398
42407
  "use strict";
42399
42408
  init_shared();
42400
- SUPPORTED_OPERATORS = ["$set", "$setOnInsert", "$unset"];
42409
+ SUPPORTED_OPERATORS = ["$set", "$setOnInsert", "$unset", "$inc"];
42401
42410
  __name(invalid, "invalid");
42402
42411
  __name(asObject, "asObject");
42403
42412
  __name(unsetFields, "unsetFields");
42413
+ __name(incFields, "incFields");
42414
+ __name(readByPath, "readByPath");
42415
+ __name(applyInc, "applyInc");
42404
42416
  __name(resolveUpdateOperators, "resolveUpdateOperators");
42405
42417
  __name(equalityFieldsOf, "equalityFieldsOf");
42406
42418
  }
@@ -42532,7 +42544,11 @@ async function handleUpdateMany(resource, params, requestId) {
42532
42544
  const oldShadowKeys = item.__shadowKeys ?? existingData.__shadowKeys ?? [];
42533
42545
  const timestampedPatch = addUpdateTimestamp(update.patch);
42534
42546
  const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), timestampedPatch);
42535
- const updatedData = { ...mergedData, id };
42547
+ const added = applyInc(mergedData, update.inc);
42548
+ const updatedData = {
42549
+ ...applyJsonMergePatch(mergedData, added),
42550
+ id
42551
+ };
42536
42552
  const newShadowRecords = generateShadowRecords(updatedData, resource, shadowConfig);
42537
42553
  const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
42538
42554
  const mainSK = generateMainRecordSK2(id);
@@ -42543,6 +42559,7 @@ async function handleUpdateMany(resource, params, requestId) {
42543
42559
  newShadowKeys,
42544
42560
  mainSK,
42545
42561
  isNew: false,
42562
+ returned: { ...update.returned, ...added },
42546
42563
  revision: revisionOf(item)
42547
42564
  });
42548
42565
  } catch (error2) {
@@ -42568,10 +42585,9 @@ async function handleUpdateMany(resource, params, requestId) {
42568
42585
  const { addCreateTimestamps: addCreateTimestamps2 } = await Promise.resolve().then(() => (init_timestamps(), timestamps_exports));
42569
42586
  for (const id of notFoundIds2) {
42570
42587
  try {
42571
- const mergedData = {
42572
- ...applyJsonMergePatch({}, { ...update.setOnInsert, ...update.patch }),
42573
- id
42574
- };
42588
+ const baseData = applyJsonMergePatch({}, { ...update.setOnInsert, ...update.patch });
42589
+ const added = applyInc(baseData, update.inc);
42590
+ const mergedData = { ...applyJsonMergePatch(baseData, added), id };
42575
42591
  const newData = addCreateTimestamps2(mergedData);
42576
42592
  const newShadowRecords = generateShadowRecords(newData, resource, shadowConfig);
42577
42593
  const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
@@ -42583,7 +42599,8 @@ async function handleUpdateMany(resource, params, requestId) {
42583
42599
  // 新規作成なので空
42584
42600
  newShadowKeys,
42585
42601
  mainSK,
42586
- isNew: true
42602
+ isNew: true,
42603
+ returned: { ...update.setOnInsert, ...update.returned, ...added }
42587
42604
  });
42588
42605
  } catch (error2) {
42589
42606
  const errorMessage = error2 instanceof Error ? error2.message : "Unknown preparation error";
@@ -42707,6 +42724,7 @@ async function handleUpdateMany(resource, params, requestId) {
42707
42724
  const failedIdsMap = {};
42708
42725
  const errorsMap = {};
42709
42726
  const successIdSet = new Set(successRecords.map((r5) => r5.id));
42727
+ const returnedById = new Map(successRecords.map((r5) => [r5.id, r5.returned]));
42710
42728
  const upsertedIds = [];
42711
42729
  for (let i5 = 0; i5 < ids.length; i5++) {
42712
42730
  const id = ids[i5];
@@ -42714,16 +42732,9 @@ async function handleUpdateMany(resource, params, requestId) {
42714
42732
  successIds[i5] = id;
42715
42733
  if (upsert && notFoundIds.includes(id)) {
42716
42734
  upsertedIds.push(id);
42717
- items.push({
42718
- id,
42719
- ...update.setOnInsert,
42720
- ...update.returned
42721
- });
42735
+ items.push({ id, ...returnedById.get(id) });
42722
42736
  } else {
42723
- items.push({
42724
- id,
42725
- ...update.returned
42726
- });
42737
+ items.push({ id, ...returnedById.get(id) });
42727
42738
  }
42728
42739
  }
42729
42740
  }
@@ -44993,7 +45004,7 @@ async function handler(event) {
44993
45004
  return createCorsResponse(HTTP_STATUS.OK);
44994
45005
  }
44995
45006
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
44996
- const version = "1.27.0";
45007
+ const version = "1.27.1";
44997
45008
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
44998
45009
  }
44999
45010
  if (event.requestContext.http.method !== "POST") {