@exabugs/dynamodb-client 1.8.0 → 1.10.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 +36 -0
- package/README.md +1 -0
- package/dist/client/FindCursor.d.ts +11 -6
- package/dist/client/FindCursor.d.ts.map +1 -1
- package/dist/client/FindCursor.js +30 -11
- package/dist/client/FindCursor.js.map +1 -1
- package/dist/integrations/react-admin/components/ReferenceManyToMany/ReferenceManyToManyField.d.ts.map +1 -1
- package/dist/integrations/react-admin/components/ReferenceManyToMany/ReferenceManyToManyField.js +5 -4
- package/dist/integrations/react-admin/components/ReferenceManyToMany/ReferenceManyToManyField.js.map +1 -1
- package/dist/integrations/react-admin/components/ReferenceManyToMany/ReferenceManyToManyInput.d.ts.map +1 -1
- package/dist/integrations/react-admin/components/ReferenceManyToMany/ReferenceManyToManyInput.js +5 -4
- package/dist/integrations/react-admin/components/ReferenceManyToMany/ReferenceManyToManyInput.js.map +1 -1
- package/dist/integrations/react-admin/components/ReferenceManyToMany/useGetListAll.d.ts +19 -0
- package/dist/integrations/react-admin/components/ReferenceManyToMany/useGetListAll.d.ts.map +1 -0
- package/dist/integrations/react-admin/components/ReferenceManyToMany/useGetListAll.js +36 -0
- package/dist/integrations/react-admin/components/ReferenceManyToMany/useGetListAll.js.map +1 -0
- package/dist/integrations/react-admin/dataProvider.d.ts +15 -1
- package/dist/integrations/react-admin/dataProvider.d.ts.map +1 -1
- package/dist/integrations/react-admin/dataProvider.js +61 -4
- package/dist/integrations/react-admin/dataProvider.js.map +1 -1
- package/dist/integrations/react-admin/index.d.ts +1 -0
- package/dist/integrations/react-admin/index.d.ts.map +1 -1
- package/dist/integrations/react-admin/index.js.map +1 -1
- package/dist/server/handler.cjs +683 -499
- package/dist/server/handler.cjs.map +4 -4
- package/dist/server/media-handler.cjs +2 -2
- package/dist/server/media-process-handler-dlq.cjs +544 -344
- package/dist/server/media-process-handler-dlq.cjs.map +4 -4
- package/dist/server/media-process-handler.cjs +543 -343
- package/dist/server/media-process-handler.cjs.map +4 -4
- package/dist/server/operations/deleteMany.d.ts.map +1 -1
- package/dist/server/operations/deleteMany.js +15 -30
- package/dist/server/operations/deleteMany.js.map +1 -1
- package/dist/server/operations/deleteOne.d.ts.map +1 -1
- package/dist/server/operations/deleteOne.js +7 -4
- package/dist/server/operations/deleteOne.js.map +1 -1
- package/dist/server/operations/find/idQuery.d.ts.map +1 -1
- package/dist/server/operations/find/idQuery.js +23 -9
- package/dist/server/operations/find/idQuery.js.map +1 -1
- package/dist/server/operations/find/nearQuery.d.ts.map +1 -1
- package/dist/server/operations/find/nearQuery.js +41 -40
- package/dist/server/operations/find/nearQuery.js.map +1 -1
- package/dist/server/operations/find/shadowQuery.d.ts +7 -0
- package/dist/server/operations/find/shadowQuery.d.ts.map +1 -1
- package/dist/server/operations/find/shadowQuery.js +34 -42
- package/dist/server/operations/find/shadowQuery.js.map +1 -1
- package/dist/server/operations/findAll.d.ts +8 -0
- package/dist/server/operations/findAll.d.ts.map +1 -0
- package/dist/server/operations/findAll.js +46 -0
- package/dist/server/operations/findAll.js.map +1 -0
- package/dist/server/operations/findMany.d.ts.map +1 -1
- package/dist/server/operations/findMany.js +9 -26
- package/dist/server/operations/findMany.js.map +1 -1
- package/dist/server/operations/findManyReference.d.ts.map +1 -1
- package/dist/server/operations/findManyReference.js +4 -14
- package/dist/server/operations/findManyReference.js.map +1 -1
- package/dist/server/operations/parameterConverter.d.ts.map +1 -1
- package/dist/server/operations/parameterConverter.js +16 -8
- package/dist/server/operations/parameterConverter.js.map +1 -1
- package/dist/server/operations/updateMany.d.ts.map +1 -1
- package/dist/server/operations/updateMany.js +15 -30
- package/dist/server/operations/updateMany.js.map +1 -1
- package/dist/server/operations/updateOne.d.ts.map +1 -1
- package/dist/server/operations/updateOne.js +8 -6
- package/dist/server/operations/updateOne.js.map +1 -1
- package/dist/server/utils/batchGet.d.ts +21 -0
- package/dist/server/utils/batchGet.d.ts.map +1 -0
- package/dist/server/utils/batchGet.js +73 -0
- package/dist/server/utils/batchGet.js.map +1 -0
- package/dist/server/utils/scanWarning.d.ts +30 -0
- package/dist/server/utils/scanWarning.d.ts.map +1 -0
- package/dist/server/utils/scanWarning.js +63 -0
- package/dist/server/utils/scanWarning.js.map +1 -0
- package/dist/shared/utils/index.d.ts +1 -0
- package/dist/shared/utils/index.d.ts.map +1 -1
- package/dist/shared/utils/index.js +1 -0
- package/dist/shared/utils/index.js.map +1 -1
- package/dist/shared/utils/scanSort.d.ts +16 -0
- package/dist/shared/utils/scanSort.d.ts.map +1 -0
- package/dist/shared/utils/scanSort.js +33 -0
- package/dist/shared/utils/scanSort.js.map +1 -0
- package/dist/sharp-layer.zip +0 -0
- package/docs/README.md +286 -0
- package/docs/SCAN_WARNING.md +81 -0
- package/package.json +2 -1
- package/terraform/README.md +8 -6
- package/terraform/main.tf +40 -1
- package/terraform/outputs.tf +13 -0
- package/terraform/variables.tf +17 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BatchGetItem の共通処理
|
|
3
|
+
*
|
|
4
|
+
* - 100 件ごとに分割する(BatchGetItem の上限)
|
|
5
|
+
* - スロットリング等で `UnprocessedKeys` が返ったら、指数バックオフで再試行して取りこぼさない
|
|
6
|
+
* - 再試行しても残る場合は、一部だけを返さずエラーにする(結果を壊さない)
|
|
7
|
+
*/
|
|
8
|
+
import { BatchGetCommand } from '@aws-sdk/lib-dynamodb';
|
|
9
|
+
import { AppError, ErrorCode, createLogger } from '../../shared/index.js';
|
|
10
|
+
import { executeDynamoDBOperation, getDBClient, getTableName } from './dynamodb.js';
|
|
11
|
+
const logger = createLogger({
|
|
12
|
+
service: 'batch-get',
|
|
13
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
14
|
+
});
|
|
15
|
+
/** BatchGetItem の 1 リクエストあたりのキー数の上限 */
|
|
16
|
+
export const BATCH_GET_MAX_KEYS = 100;
|
|
17
|
+
/** UnprocessedKeys の再試行回数の上限 */
|
|
18
|
+
export const BATCH_GET_MAX_RETRIES = 8;
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* キーに対応するレコードを全件取得する(重複キーは 1 回だけ取得する。順序は保証しない)
|
|
24
|
+
*/
|
|
25
|
+
export async function batchGetAll(keys, { consistentRead, costTracker, requestId, baseDelayMs = 50 }) {
|
|
26
|
+
const unique = Array.from(new Map(keys.map((k) => [`${k.PK}\u0000${k.SK}`, k])).values());
|
|
27
|
+
if (unique.length === 0) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const dbClient = getDBClient();
|
|
31
|
+
const tableName = getTableName();
|
|
32
|
+
const records = [];
|
|
33
|
+
for (let i = 0; i < unique.length; i += BATCH_GET_MAX_KEYS) {
|
|
34
|
+
let pending = unique.slice(i, i + BATCH_GET_MAX_KEYS);
|
|
35
|
+
for (let attempt = 0; pending.length > 0; attempt++) {
|
|
36
|
+
if (attempt > BATCH_GET_MAX_RETRIES) {
|
|
37
|
+
// 一部だけを返すと、呼び出し側が「存在しない」と誤認する(更新・削除の対象から漏れる等)
|
|
38
|
+
logger.error('BatchGetItem の UnprocessedKeys が解消しなかった', {
|
|
39
|
+
requestId,
|
|
40
|
+
unprocessed: pending.length,
|
|
41
|
+
attempts: attempt,
|
|
42
|
+
});
|
|
43
|
+
throw new AppError(ErrorCode.INTERNAL_ERROR, 'BatchGetItem still has unprocessed keys after retries', 503, { unprocessed: pending.length });
|
|
44
|
+
}
|
|
45
|
+
if (attempt > 0) {
|
|
46
|
+
await sleep(baseDelayMs * 2 ** (attempt - 1) * (0.5 + Math.random() / 2));
|
|
47
|
+
}
|
|
48
|
+
const keysToGet = pending;
|
|
49
|
+
const result = await executeDynamoDBOperation(() => dbClient.send(new BatchGetCommand({
|
|
50
|
+
RequestItems: {
|
|
51
|
+
[tableName]: {
|
|
52
|
+
Keys: keysToGet,
|
|
53
|
+
ConsistentRead: consistentRead,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
ReturnConsumedCapacity: 'TOTAL',
|
|
57
|
+
})), 'BatchGetItem');
|
|
58
|
+
if (costTracker) {
|
|
59
|
+
const capacity = result.ConsumedCapacity;
|
|
60
|
+
if (Array.isArray(capacity)) {
|
|
61
|
+
capacity.forEach((c) => costTracker.add(c));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
costTracker.add(capacity);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
records.push(...(result.Responses?.[tableName] ?? []));
|
|
68
|
+
pending = result.UnprocessedKeys?.[tableName]?.Keys ?? [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return records;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=batchGet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batchGet.js","sourceRoot":"","sources":["../../../src/server/utils/batchGet.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** CloudWatch のメトリクスフィルタが検知するイベント名 */
|
|
2
|
+
export declare const SPARSE_SCAN_EVENT = "SPARSE_SCAN";
|
|
3
|
+
/** 閾値の既定値(1リクエストで読む件数) */
|
|
4
|
+
export declare const DEFAULT_SCAN_WARN_THRESHOLD = 5000;
|
|
5
|
+
/**
|
|
6
|
+
* 警告の閾値を取得する(環境変数 SCAN_WARN_THRESHOLD。1 以上の整数。不正な値は既定値)
|
|
7
|
+
*/
|
|
8
|
+
export declare function getScanWarnThreshold(): number;
|
|
9
|
+
export interface ScanWatcherContext {
|
|
10
|
+
requestId: string;
|
|
11
|
+
resource: string;
|
|
12
|
+
/** 走査の種類(どの経路で疎な検索になったか) */
|
|
13
|
+
path: 'id-filtered-scan' | 'shadow-filter-first' | 'shadow-full-scan' | 'near-search';
|
|
14
|
+
sortField: string;
|
|
15
|
+
/** 絞り込みに使われたフィールドと演算子(例: "name:$contains") */
|
|
16
|
+
filters: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 1リクエスト内で読んだ件数を数え、閾値を超えたら警告を 1 回だけ記録する
|
|
20
|
+
*/
|
|
21
|
+
export declare class ScanWatcher {
|
|
22
|
+
private readonly context;
|
|
23
|
+
private scanned;
|
|
24
|
+
private warned;
|
|
25
|
+
private readonly threshold;
|
|
26
|
+
constructor(context: ScanWatcherContext);
|
|
27
|
+
/** 読んだ件数を加える */
|
|
28
|
+
add(count: number): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=scanWarning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanWarning.d.ts","sourceRoot":"","sources":["../../../src/server/utils/scanWarning.ts"],"names":[],"mappings":"AAkBA,sCAAsC;AACtC,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAE/C,0BAA0B;AAC1B,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAO7C;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,IAAI,EAAE,kBAAkB,GAAG,qBAAqB,GAAG,kBAAkB,GAAG,aAAa,CAAC;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,WAAW;IAKV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAEvB,OAAO,EAAE,kBAAkB;IAExD,gBAAgB;IAChB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAqBzB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 疎な検索(SPARSE_SCAN)の警告
|
|
3
|
+
*
|
|
4
|
+
* インデックス(シャドー)で絞り込めない条件の検索は、条件に一致する件数に対して
|
|
5
|
+
* 多くのレコードを読む必要がある。結果を正しく返すために途中で打ち切ることはせず、
|
|
6
|
+
* 1リクエストで読んだ件数が閾値を超えたときに警告を記録して、利用者が
|
|
7
|
+
* インデックス・スキーマ・ロジックの見直しに着手できるようにする。
|
|
8
|
+
*
|
|
9
|
+
* - 閾値: 環境変数 SCAN_WARN_THRESHOLD(1リクエストで読む件数。既定 5000)
|
|
10
|
+
* - 警告は 1 リクエストにつき 1 回、閾値を超えた時点で出す(Lambda がタイムアウトしても残る)
|
|
11
|
+
* - ログは JSON で `event: "SPARSE_SCAN"` を持つ。CloudWatch のメトリクスフィルタで検知できる
|
|
12
|
+
* (terraform/ に定義済み。詳細は docs/SCAN_WARNING.md)
|
|
13
|
+
*/
|
|
14
|
+
import { createLogger } from '../../shared/index.js';
|
|
15
|
+
// 警告は気づくための仕組みなので、LOG_LEVEL(error など)に関わらず必ず出力する
|
|
16
|
+
const logger = createLogger({ service: 'scan-warning', level: 'warn' });
|
|
17
|
+
/** CloudWatch のメトリクスフィルタが検知するイベント名 */
|
|
18
|
+
export const SPARSE_SCAN_EVENT = 'SPARSE_SCAN';
|
|
19
|
+
/** 閾値の既定値(1リクエストで読む件数) */
|
|
20
|
+
export const DEFAULT_SCAN_WARN_THRESHOLD = 5000;
|
|
21
|
+
/**
|
|
22
|
+
* 警告の閾値を取得する(環境変数 SCAN_WARN_THRESHOLD。1 以上の整数。不正な値は既定値)
|
|
23
|
+
*/
|
|
24
|
+
export function getScanWarnThreshold() {
|
|
25
|
+
const raw = process.env.SCAN_WARN_THRESHOLD;
|
|
26
|
+
if (raw === undefined || raw === '') {
|
|
27
|
+
return DEFAULT_SCAN_WARN_THRESHOLD;
|
|
28
|
+
}
|
|
29
|
+
const value = Number(raw);
|
|
30
|
+
return Number.isInteger(value) && value >= 1 ? value : DEFAULT_SCAN_WARN_THRESHOLD;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 1リクエスト内で読んだ件数を数え、閾値を超えたら警告を 1 回だけ記録する
|
|
34
|
+
*/
|
|
35
|
+
export class ScanWatcher {
|
|
36
|
+
context;
|
|
37
|
+
scanned = 0;
|
|
38
|
+
warned = false;
|
|
39
|
+
threshold = getScanWarnThreshold();
|
|
40
|
+
constructor(context) {
|
|
41
|
+
this.context = context;
|
|
42
|
+
}
|
|
43
|
+
/** 読んだ件数を加える */
|
|
44
|
+
add(count) {
|
|
45
|
+
this.scanned += count;
|
|
46
|
+
if (!this.warned && this.scanned >= this.threshold) {
|
|
47
|
+
this.warned = true;
|
|
48
|
+
logger.warn(`${SPARSE_SCAN_EVENT}: 1リクエストで ${this.scanned} 件以上を読みました。` +
|
|
49
|
+
`インデックス・スキーマ・ロジックの見直しを検討してください(docs/SCAN_WARNING.md)`, {
|
|
50
|
+
event: SPARSE_SCAN_EVENT,
|
|
51
|
+
...this.context,
|
|
52
|
+
scanned: this.scanned,
|
|
53
|
+
threshold: this.threshold,
|
|
54
|
+
actions: [
|
|
55
|
+
'インデックス: 絞り込みに使うフィールドをシャドー設定に追加し、$eq などインデックスで解決できる条件にする',
|
|
56
|
+
'スキーマ: 絞り込み条件をキーにできるよう、コレクションの分割・非正規化を検討する',
|
|
57
|
+
'ロジック: 絞り込みを毎回行う必要があるか、条件を変えられないか、結果をキャッシュできないかを見直す',
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=scanWarning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanWarning.js","sourceRoot":"","sources":["../../../src/server/utils/scanWarning.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,iDAAiD;AACjD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;AAExE,sCAAsC;AACtC,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAE/C,0BAA0B;AAC1B,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAC5C,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QACpC,OAAO,2BAA2B,CAAC;IACrC,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,2BAA2B,CAAC;AACrF,CAAC;AAYD;;GAEG;AACH,MAAM,OAAO,WAAW;IAKO;IAJrB,OAAO,GAAG,CAAC,CAAC;IACZ,MAAM,GAAG,KAAK,CAAC;IACN,SAAS,GAAG,oBAAoB,EAAE,CAAC;IAEpD,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D,gBAAgB;IAChB,GAAG,CAAC,KAAa;QACf,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,MAAM,CAAC,IAAI,CACT,GAAG,iBAAiB,aAAa,IAAI,CAAC,OAAO,aAAa;gBACxD,qDAAqD,EACvD;gBACE,KAAK,EAAE,iBAAiB;gBACxB,GAAG,IAAI,CAAC,OAAO;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,OAAO,EAAE;oBACP,yDAAyD;oBACzD,2CAA2C;oBAC3C,oDAAoD;iBACrD;aACF,CACF,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全件読み取りの並べ替えフィールドを選ぶ
|
|
3
|
+
*
|
|
4
|
+
* フィルタが「単一フィールドの等価($eq)で、値が短い普通の文字列」だけなら、そのフィールドを返す。
|
|
5
|
+
* シャドーのキー条件で一致するレコードだけを読めるので(O(一致件数))、大きなコレクションでも
|
|
6
|
+
* 疎な検索(SPARSE_SCAN)にならない。それ以外は不変の `id`(カーソル方式。全件で O(件数))。
|
|
7
|
+
*
|
|
8
|
+
* 数値・真偽値・長い文字列・日時形式の文字列は対象外。シャドーの SK は書き込み時に型ごとに変換される
|
|
9
|
+
* (数値はオフセット付き、真偽値は 1/0、日時形式の文字列は toISOString、長い文字列は
|
|
10
|
+
* SHADOW_STRING_MAX_BYTES で切り詰め)が、検索側の $eq は同じ変換をしないため、キー条件が一致せず
|
|
11
|
+
* 0 件になりうる(#57)。長さの上限はサーバー設定(既定 100 バイト)より十分小さい 64 バイトにしている。
|
|
12
|
+
*
|
|
13
|
+
* 注意: 読んでいる最中にそのフィールドの値が書き換わると取りこぼしうる(外部キーは通常変更されない)。
|
|
14
|
+
*/
|
|
15
|
+
export declare function chooseScanSortField(filter: Record<string, unknown>): string;
|
|
16
|
+
//# sourceMappingURL=scanSort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanSort.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/scanSort.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAkB3E"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全件読み取りの並べ替えフィールドを選ぶ
|
|
3
|
+
*
|
|
4
|
+
* フィルタが「単一フィールドの等価($eq)で、値が短い普通の文字列」だけなら、そのフィールドを返す。
|
|
5
|
+
* シャドーのキー条件で一致するレコードだけを読めるので(O(一致件数))、大きなコレクションでも
|
|
6
|
+
* 疎な検索(SPARSE_SCAN)にならない。それ以外は不変の `id`(カーソル方式。全件で O(件数))。
|
|
7
|
+
*
|
|
8
|
+
* 数値・真偽値・長い文字列・日時形式の文字列は対象外。シャドーの SK は書き込み時に型ごとに変換される
|
|
9
|
+
* (数値はオフセット付き、真偽値は 1/0、日時形式の文字列は toISOString、長い文字列は
|
|
10
|
+
* SHADOW_STRING_MAX_BYTES で切り詰め)が、検索側の $eq は同じ変換をしないため、キー条件が一致せず
|
|
11
|
+
* 0 件になりうる(#57)。長さの上限はサーバー設定(既定 100 バイト)より十分小さい 64 バイトにしている。
|
|
12
|
+
*
|
|
13
|
+
* 注意: 読んでいる最中にそのフィールドの値が書き換わると取りこぼしうる(外部キーは通常変更されない)。
|
|
14
|
+
*/
|
|
15
|
+
export function chooseScanSortField(filter) {
|
|
16
|
+
const keys = Object.keys(filter);
|
|
17
|
+
if (keys.length !== 1) {
|
|
18
|
+
return 'id';
|
|
19
|
+
}
|
|
20
|
+
const [key] = keys;
|
|
21
|
+
const value = filter[key];
|
|
22
|
+
const isPlainField = !key.includes(':') && !key.includes('.') && key !== 'id';
|
|
23
|
+
const isSafeString = typeof value === 'string' &&
|
|
24
|
+
value.length > 0 &&
|
|
25
|
+
new TextEncoder().encode(value).length <= 64 &&
|
|
26
|
+
// 空白や '#' はシャドーの SK でエスケープされ、'id' を含む値は SK の区切りと紛らわしくなる
|
|
27
|
+
// (extractRecordIds は最後の '#id#' で分けるが、念のため避ける)
|
|
28
|
+
!/[\s#]/.test(value) &&
|
|
29
|
+
value !== 'id' &&
|
|
30
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value);
|
|
31
|
+
return isPlainField && isSafeString ? key : 'id';
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=scanSort.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanSort.js","sourceRoot":"","sources":["../../../src/shared/utils/scanSort.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA+B;IACjE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,IAAI,CAAC;IAC9E,MAAM,YAAY,GAChB,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,EAAE;QAC5C,wDAAwD;QACxD,+CAA+C;QAC/C,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QACpB,KAAK,KAAK,IAAI;QACd,CAAC,sCAAsC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtD,OAAO,YAAY,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC"}
|
package/dist/sharp-layer.zip
CHANGED
|
Binary file
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# @exabugs/dynamodb-client
|
|
2
|
+
|
|
3
|
+
DynamoDB Single-Table設計向けのクライアントライブラリ。MongoDB風のAPIでDynamoDBを操作できます。
|
|
4
|
+
|
|
5
|
+
## 特徴
|
|
6
|
+
|
|
7
|
+
- **認証方式別クライアント**: IAM、Cognito、Token認証に最適化
|
|
8
|
+
- **型安全**: TypeScriptで完全に型付けされたAPI
|
|
9
|
+
- **軽量**: 認証方式ごとに必要な依存関係のみを含む
|
|
10
|
+
- **MongoDB風API**: 使いやすいCRUD操作
|
|
11
|
+
|
|
12
|
+
## インストール
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @exabugs/dynamodb-client
|
|
16
|
+
# または
|
|
17
|
+
pnpm add @exabugs/dynamodb-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## クイックスタート
|
|
21
|
+
|
|
22
|
+
### Cognito認証(ブラウザ/Node.js)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { DynamoClient } from '@exabugs/dynamodb-client/client/cognito';
|
|
26
|
+
|
|
27
|
+
const client = new DynamoClient(FUNCTION_URL, {
|
|
28
|
+
auth: {
|
|
29
|
+
getToken: async () => {
|
|
30
|
+
// Cognitoトークンを取得
|
|
31
|
+
return token;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await client.connect();
|
|
37
|
+
|
|
38
|
+
const db = client.db('mydb');
|
|
39
|
+
const articles = db.collection('articles');
|
|
40
|
+
|
|
41
|
+
// CRUD操作
|
|
42
|
+
const results = await articles
|
|
43
|
+
.find({ status: 'published' })
|
|
44
|
+
.sort({ publishedAt: 'desc' })
|
|
45
|
+
.limit(10)
|
|
46
|
+
.toArray();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### IAM認証(Lambda/Node.js)
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { DynamoClient } from '@exabugs/dynamodb-client/client/iam';
|
|
53
|
+
|
|
54
|
+
const client = new DynamoClient(FUNCTION_URL, {
|
|
55
|
+
auth: {
|
|
56
|
+
region: process.env.AWS_REGION!,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await client.connect();
|
|
61
|
+
|
|
62
|
+
const db = client.db('mydb');
|
|
63
|
+
const articles = db.collection('articles');
|
|
64
|
+
|
|
65
|
+
await articles.insertOne({
|
|
66
|
+
title: 'New Article',
|
|
67
|
+
status: 'draft',
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Token認証(固定トークン)
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { DynamoClient } from '@exabugs/dynamodb-client/client/token';
|
|
75
|
+
|
|
76
|
+
const client = new DynamoClient(FUNCTION_URL, {
|
|
77
|
+
auth: {
|
|
78
|
+
token: 'your-static-token',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await client.connect();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Records Lambda(サーバー側実装)
|
|
86
|
+
|
|
87
|
+
このライブラリには、Records Lambda(サーバー側実装)も含まれています。
|
|
88
|
+
|
|
89
|
+
### デプロイ方法
|
|
90
|
+
|
|
91
|
+
Terraformモジュールを使用してデプロイします。
|
|
92
|
+
|
|
93
|
+
```hcl
|
|
94
|
+
module "lambda_records" {
|
|
95
|
+
source = "github.com/your-org/dynamodb-client//packages/core/terraform"
|
|
96
|
+
# または
|
|
97
|
+
# source = "../packages/core/terraform" # ローカル開発時
|
|
98
|
+
|
|
99
|
+
project_name = "my-project"
|
|
100
|
+
environment = "dev"
|
|
101
|
+
region = "us-east-1"
|
|
102
|
+
|
|
103
|
+
# DynamoDB設定
|
|
104
|
+
dynamodb_table_name = "my-table"
|
|
105
|
+
dynamodb_table_arn = "arn:aws:dynamodb:..."
|
|
106
|
+
|
|
107
|
+
# Cognito設定
|
|
108
|
+
cognito_user_pool_id = "us-east-1_xxxxx"
|
|
109
|
+
|
|
110
|
+
# シャドウ設定(base64エンコード)
|
|
111
|
+
shadow_config = base64encode(file("shadow.config.json"))
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### ビルド
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd packages/core
|
|
119
|
+
pnpm build # クライアントとサーバーの両方をビルド
|
|
120
|
+
pnpm build:lambda # サーバーのみをビルド
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
詳細は[TerraformモジュールのREADME](./terraform/README.md)を参照してください。
|
|
124
|
+
|
|
125
|
+
## メンテナンススクリプト
|
|
126
|
+
|
|
127
|
+
### シャドーレコード修復
|
|
128
|
+
|
|
129
|
+
DynamoClient(IAM認証)を使用してシャドーレコードを修復するスクリプトです。
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Dry run(変更なし)
|
|
133
|
+
pnpm repair-shadows -- --resource articles --dry-run
|
|
134
|
+
|
|
135
|
+
# 単一レコードを修復
|
|
136
|
+
pnpm repair-shadows -- --resource articles --id <RECORD_ID> --repair
|
|
137
|
+
|
|
138
|
+
# 全レコードを修復
|
|
139
|
+
pnpm repair-shadows -- --resource articles --repair
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
環境変数:
|
|
143
|
+
|
|
144
|
+
- `ENV`: 環境(dev/stg/prd)、デフォルト: `dev`
|
|
145
|
+
- `AWS_REGION`: AWSリージョン、デフォルト: `us-east-1`
|
|
146
|
+
- `RECORDS_API_URL`: Records Lambda Function URL(省略時はAWS CLIで自動取得)
|
|
147
|
+
|
|
148
|
+
## ドキュメント
|
|
149
|
+
|
|
150
|
+
- [使用ガイド](./CLIENT_USAGE.md) - 認証方式別の詳細な使用方法
|
|
151
|
+
- [アーキテクチャ](./ARCHITECTURE.md) - 内部設計とコンポーネント構成
|
|
152
|
+
|
|
153
|
+
## API型定義
|
|
154
|
+
|
|
155
|
+
Records Lambda API の型定義は `@exabugs/dynamodb-client/types` からエクスポートされています。
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import type {
|
|
159
|
+
ApiErrorResponse, // 操作タイプ
|
|
160
|
+
ApiOperation, // リクエスト型
|
|
161
|
+
ApiRequest,
|
|
162
|
+
ApiResponse, // レスポンス型
|
|
163
|
+
ApiSuccessResponse,
|
|
164
|
+
BulkOperationResult,
|
|
165
|
+
DeleteOneParams,
|
|
166
|
+
FindManyParams,
|
|
167
|
+
FindOneParams,
|
|
168
|
+
FindParams, // データ型
|
|
169
|
+
FindResult,
|
|
170
|
+
InsertOneParams,
|
|
171
|
+
OperationError,
|
|
172
|
+
UpdateOneParams,
|
|
173
|
+
} from '@exabugs/dynamodb-client/types';
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## シャドウ管理機能
|
|
177
|
+
|
|
178
|
+
このライブラリには、DynamoDB Single-Table設計におけるシャドウレコード管理機能が含まれています。
|
|
179
|
+
|
|
180
|
+
### インストール
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pnpm add @exabugs/dynamodb-client
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 使用方法
|
|
187
|
+
|
|
188
|
+
#### シャドウSK生成
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { generateShadowSK } from '@exabugs/dynamodb-client/shadows';
|
|
192
|
+
|
|
193
|
+
// 文字列フィールド
|
|
194
|
+
const nameSK = generateShadowSK('name', 'Tech News', '01HZXY123', 'string');
|
|
195
|
+
// => 'name#Tech#News#id#01HZXY123'
|
|
196
|
+
|
|
197
|
+
// 数値フィールド
|
|
198
|
+
const prioritySK = generateShadowSK('priority', 123, '01HZXY123', 'number');
|
|
199
|
+
// => 'priority#00000000000000000123#id#01HZXY123'
|
|
200
|
+
|
|
201
|
+
// 日時フィールド
|
|
202
|
+
const createdAtSK = generateShadowSK(
|
|
203
|
+
'createdAt',
|
|
204
|
+
'2025-11-12T10:00:00.000Z',
|
|
205
|
+
'01HZXY123',
|
|
206
|
+
'datetime'
|
|
207
|
+
);
|
|
208
|
+
// => 'createdAt#2025-11-12T10:00:00.000Z#id#01HZXY123'
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### シャドウ差分計算
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { calculateShadowDiff } from '@exabugs/dynamodb-client/shadows';
|
|
215
|
+
|
|
216
|
+
const oldKeys = ['name#Old#Name#id#01HZXY123', 'priority#00000000000000000010#id#01HZXY123'];
|
|
217
|
+
|
|
218
|
+
const newKeys = ['name#New#Name#id#01HZXY123', 'priority#00000000000000000010#id#01HZXY123'];
|
|
219
|
+
|
|
220
|
+
const diff = calculateShadowDiff(oldKeys, newKeys);
|
|
221
|
+
// => {
|
|
222
|
+
// toDelete: ['name#Old#Name#id#01HZXY123'],
|
|
223
|
+
// toAdd: ['name#New#Name#id#01HZXY123']
|
|
224
|
+
// }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### 設定管理
|
|
228
|
+
|
|
229
|
+
v0.3.x以降、shadow.config.jsonファイルは不要になりました。すべてのフィールドが自動的にシャドウ化されます。
|
|
230
|
+
|
|
231
|
+
環境変数で動作をカスタマイズできます:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// 環境変数(デフォルト値)
|
|
235
|
+
process.env.SHADOW_CREATED_AT_FIELD = 'createdAt'; // 作成日時フィールド名
|
|
236
|
+
process.env.SHADOW_UPDATED_AT_FIELD = 'updatedAt'; // 更新日時フィールド名
|
|
237
|
+
process.env.SHADOW_STRING_MAX_BYTES = '100'; // 文字列の最大バイト数
|
|
238
|
+
process.env.SHADOW_NUMBER_PADDING = '15'; // 数値のパディング桁数
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
シャドウレコードの生成例:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { generateShadowSK } from '@exabugs/dynamodb-client/shadows';
|
|
245
|
+
|
|
246
|
+
// 各フィールドタイプのシャドウSK生成
|
|
247
|
+
const nameSK = generateShadowSK('name', 'Tech News', '01HZXY123', 'string');
|
|
248
|
+
// => 'name#Tech#News#id#01HZXY123'
|
|
249
|
+
|
|
250
|
+
const prioritySK = generateShadowSK('priority', 123, '01HZXY123', 'number');
|
|
251
|
+
// => 'priority#00000000000000000123#id#01HZXY123'
|
|
252
|
+
|
|
253
|
+
const dateSK = generateShadowSK('createdAt', '2025-11-12T10:00:00.000Z', '01HZXY123', 'datetime');
|
|
254
|
+
// => 'createdAt#2025-11-12T10:00:00.000Z#id#01HZXY123'
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### エスケープルール
|
|
258
|
+
|
|
259
|
+
文字列値は以下のルールでエスケープされます:
|
|
260
|
+
|
|
261
|
+
- `#` → `##`
|
|
262
|
+
- スペース → `#`
|
|
263
|
+
|
|
264
|
+
例:
|
|
265
|
+
|
|
266
|
+
- `"Tech News"` → `"Tech#News"`
|
|
267
|
+
- `"AI#ML"` → `"AI##ML"`
|
|
268
|
+
|
|
269
|
+
## パッケージ構成
|
|
270
|
+
|
|
271
|
+
| エクスポート | 環境 | 用途 | バンドルサイズ |
|
|
272
|
+
| ----------------------------------------- | ------- | ------------------------------ | -------------- |
|
|
273
|
+
| `@exabugs/dynamodb-client/client/iam` | Node.js | IAM認証クライアント | ~500KB |
|
|
274
|
+
| `@exabugs/dynamodb-client/client/cognito` | 共通 | Cognito認証クライアント | ~50KB |
|
|
275
|
+
| `@exabugs/dynamodb-client/client/token` | 共通 | Token認証クライアント | ~50KB |
|
|
276
|
+
| `@exabugs/dynamodb-client/server` | Node.js | Records Lambda(サーバー実装) | ~1.2MB |
|
|
277
|
+
| `@exabugs/dynamodb-client/server/handler` | Node.js | Records Lambda ハンドラー | ~1.2MB |
|
|
278
|
+
| `@exabugs/dynamodb-client/shadows` | Node.js | シャドウ管理機能 | ~10KB |
|
|
279
|
+
| `@exabugs/dynamodb-client/types` | 共通 | API型定義 | - |
|
|
280
|
+
| `@exabugs/dynamodb-client/terraform` | - | Terraformモジュール | - |
|
|
281
|
+
|
|
282
|
+
## ライセンス
|
|
283
|
+
|
|
284
|
+
MIT License - 詳細は [LICENSE](LICENSE) ファイルを参照してください。
|
|
285
|
+
|
|
286
|
+
Copyright (c) 2024 exabugs
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 疎な検索の警告(SPARSE_SCAN)と対応
|
|
2
|
+
|
|
3
|
+
## 方針
|
|
4
|
+
|
|
5
|
+
インデックス(シャドー)で絞り込めない条件の検索は、一致する件数に対して多くのレコードを読む必要がある。
|
|
6
|
+
**結果を正しく返すため、検索を途中で打ち切ることはしない**(途中までを「全件」「最終ページ」として返すと、
|
|
7
|
+
呼び出し側が結果を誤って扱い、データの欠落や関連の誤削除につながる)。
|
|
8
|
+
|
|
9
|
+
代わりに、1リクエストで読んだ件数が閾値を超えたとき**警告を記録**する。警告に気づいて、
|
|
10
|
+
インデックス・スキーマ・ロジックの見直しに着手することが目的。
|
|
11
|
+
|
|
12
|
+
## 何が警告されるか
|
|
13
|
+
|
|
14
|
+
Records Lambda が、次の経路で 1 リクエストあたりの読み取り件数を数える。
|
|
15
|
+
|
|
16
|
+
| `path` | どんな検索か |
|
|
17
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `id-filtered-scan` | `sort: id`(react-admin の既定)に **id 以外のフィールドの絞り込み**を付けた検索。演算子(`$eq` を含む)によらず、id 順に全件を読んでメモリで絞る |
|
|
19
|
+
| `shadow-filter-first` | 並べ替えフィールドとは別のフィールドを `$eq` で先に絞り(`schema.cardinality` が perPage/2 を超える場合のみ)、残りの条件をメモリで絞る検索。全件をメモリに載せる |
|
|
20
|
+
| `shadow-full-scan` | 並べ替えフィールド以外への絞り込みで、`schema.cardinality` が未設定/小さい `$eq`、または `$contains` `$ne` `$in` `$regex` など(並べ替えフィールド自身へのこれらの演算子も含む)。並べ替えフィールドの全件をメモリに載せる |
|
|
21
|
+
|
|
22
|
+
閾値は環境変数 `SCAN_WARN_THRESHOLD`(既定 5000 件)。Terraform では `scan_warn_threshold`。
|
|
23
|
+
閾値に達した時点で 1 リクエストにつき 1 回、次の JSON を `warn` で出す(Lambda がタイムアウトしても残る)。
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"level": "warn",
|
|
28
|
+
"event": "SPARSE_SCAN",
|
|
29
|
+
"resource": "venues",
|
|
30
|
+
"path": "id-filtered-scan",
|
|
31
|
+
"sortField": "id",
|
|
32
|
+
"filters": ["name:$contains"],
|
|
33
|
+
"scanned": 5000,
|
|
34
|
+
"threshold": 5000,
|
|
35
|
+
"requestId": "…",
|
|
36
|
+
"actions": ["インデックス: …", "スキーマ: …", "ロジック: …"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
クライアントの `toArrayAll()` も、100 ページ(既定。`warnPages` で変更)を超えるとコンソールに警告する。
|
|
41
|
+
こちらも取得は打ち切らない。`maxPages` を**明示した**場合のみ、超過時にエラーを投げる(黙って切り詰めない)。
|
|
42
|
+
|
|
43
|
+
## 警告に気づく
|
|
44
|
+
|
|
45
|
+
`terraform/` を使っている場合、`event = "SPARSE_SCAN"` のログをメトリクス
|
|
46
|
+
`<project>/<env>/Records` の `SparseScanCount` にし、CloudWatch アラーム
|
|
47
|
+
`<project>-<env>-records-sparse-scan` を作る。通知するには `scan_warning_alarm_actions` に
|
|
48
|
+
SNS トピックの ARN を渡す。Terraform を使わない場合は、同じ条件でメトリクスフィルタを作る。
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
{ $.event = "SPARSE_SCAN" }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 警告が出たら
|
|
55
|
+
|
|
56
|
+
ログの `resource` / `path` / `filters` / `sortField` から、どの検索かを特定して見直す。
|
|
57
|
+
|
|
58
|
+
1. **インデックスを見直す**
|
|
59
|
+
- 全フィールドは自動でシャドー(インデックス)化されている。効くかどうかは**並べ替えフィールドと演算子**で決まる
|
|
60
|
+
- **絞り込みに使うフィールドで並べ替える。** 並べ替えフィールドへの `$eq` `$gt` `$gte` `$lt` `$lte` `$starts` は、キー条件として直接読み取れる。`sort: id` とフィルタの組み合わせ(`id-filtered-scan`)は、どの演算子でも全件走査になるので避ける
|
|
61
|
+
- 並べ替えと別のフィールドを `$eq` で絞る場合は、`schema.cardinality` にそのフィールドの値の種類数を設定する(perPage/2 を超えると、そのフィールドで先に絞る filter-first が使われる)
|
|
62
|
+
- `$contains` `$ne` `$in` `$regex` などはインデックスを使えない。`$eq` / `$starts` / 範囲に置き換えられないか検討する
|
|
63
|
+
2. **スキーマを見直す**
|
|
64
|
+
- 絞り込み条件で値を分けられるなら、コレクションを分ける、または条件を値に持つ別フィールド(検索用の接頭辞付きの値など)を追加する(非正規化)
|
|
65
|
+
- 多対多の中間テーブルなど、関連が増え続ける構造は、片側を並べ替えフィールドにして絞れる形にする
|
|
66
|
+
3. **ロジックを見直す**
|
|
67
|
+
- その絞り込みを毎回行う必要があるか、条件を変えられないか、結果をキャッシュできないかを確認する
|
|
68
|
+
- 全件が必要な処理(バッチ・修復スクリプトなど)は、管理画面や API の通常経路ではなく、
|
|
69
|
+
`sort({ id: 'asc' }).toArrayAll()`(id 順のカーソル方式。全件で O(件数))を使うバッチとして分ける。
|
|
70
|
+
オフセット方式の経路(`shadow-*`)で `toArrayAll` を使うと、ページごとに全件を読み直すため読み取り量が件数の二乗になる
|
|
71
|
+
|
|
72
|
+
見直した後は、同じ検索で警告が出なくなったことをログで確認する。閾値は、
|
|
73
|
+
アプリの通常の最大データ量に合わせて調整してよい(上げて警告を黙らせるのではなく、見直しを優先する)。
|
|
74
|
+
|
|
75
|
+
## 補足
|
|
76
|
+
|
|
77
|
+
- 警告は `LOG_LEVEL` に関わらず出力される(`LOG_LEVEL=error` でも出る)
|
|
78
|
+
- `scan_warning_alarm_actions` が空のままだと、アラーム状態が記録されるだけで誰にも通知されない。
|
|
79
|
+
気づくためには SNS トピックなどの通知先を渡すこと
|
|
80
|
+
- アラームとメトリクスの作成には、デプロイ用ロールに `logs:PutMetricFilter` `cloudwatch:PutMetricAlarm`
|
|
81
|
+
`cloudwatch:TagResource` が必要
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exabugs/dynamodb-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "DynamoDB Single-Table Client SDK with MongoDB-like API, Shadow Records, and Lambda implementation for serverless applications",
|
|
5
5
|
"author": "exabugs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"bin",
|
|
39
39
|
"dist",
|
|
40
40
|
"terraform",
|
|
41
|
+
"docs/SCAN_WARNING.md",
|
|
41
42
|
"README.md",
|
|
42
43
|
"LICENSE",
|
|
43
44
|
"CHANGELOG.md"
|