@exabugs/dynamodb-client 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.18.0] - 2026-10-24
11
+
12
+ ### Added
13
+
14
+ - **`createAlertsCleanupHandler(options?)`**(`@exabugs/dynamodb-client/notify`。ADR 0006 の段階 3 = (e)): alerts の定時削除 Lambda のハンドラのファクトリ。asanowa・astro に複製されていた入口(`runCleanup` と環境変数の確認・接続)を 1 つにした。呼び出しごとに `withCollection('alerts', …)` で Records に接続し、`cleanupAlerts` を 1 回行い、結果を構造化ログに出す(失敗は error `ALERTS_CLEANUP_FAILED`、上限で止めたら warn `ALERTS_CLEANUP_BACKLOG`、それ以外は info。削除の失敗は例外にしない)。アプリ名・Records の URL はオプションか環境変数(`PROJECT_NAME` / `RECORDS_IAM_URL`)。読み取りの包み方(`collection`。権限チェック層を通すアプリ用)、保存期間(`olderThanDays`)、1 回の上限(`maxPerRun`)、ログ(`log`)を注入できる。使い方は `docs/NOTIFY.md`
15
+ - `runAlertsCleanup(collection, options)`(削除 1 回と結果のログ)、定数 `ALERTS_CLEANUP_MAX_PER_RUN`(500)、型 `AlertsCleanupHandlerOptions` / `RunAlertsCleanupOptions` / `AlertsCleanupLog` / `AlertsCleanupRow`
16
+
17
+ ### Changed
18
+
19
+ - 統合テスト(DynamoDB Local)の土台を `src/__tests__/helpers/recordsLocal.ts` に共通化した(テストのみ。公開 API・dist に変更なし)。DynamoDB アクセスの許可リストは、`src/system-changes/__tests__/*.integration.test.ts` をこの 1 ファイルに置き換えた。`tsconfig.json` はビルドから `**/__tests__/**` を除く(今は該当する非テストのファイルがこの土台だけで、dist は変わらない)
20
+
10
21
  ## [1.17.0] - 2026-10-24
11
22
 
12
23
  ### Added
@@ -0,0 +1,63 @@
1
+ /**
2
+ * alerts の定時削除 Lambda のハンドラのファクトリ(ADR 0006 の (e)。ADR 0003 の保存期間)。
3
+ *
4
+ * 運用通知の記録(alerts)のうち、最後の発生から保存期間(既定 28 日)を過ぎたものを消す
5
+ * (ネイティブ TTL は効かない)。削除そのものは `cleanupAlerts`(変えない)。ここは、その周りの定型
6
+ * (1 回の上限、結果の構造化ログ、環境変数の確認、Records への接続と close)を持つ。docs/NOTIFY.md
7
+ *
8
+ * 設定は、オプションが環境変数より優先される(どちらも呼び出しのたびに読む):
9
+ * - `app`: PROJECT_NAME(必須)
10
+ * - `records.url` / `records.region`: RECORDS_IAM_URL(必須)/ AWS_REGION
11
+ *
12
+ * 失敗の扱い: 削除の失敗は握りつぶさず、構造化ログ(error。target は alerts)に出す。例外にはしない
13
+ * (このロググループは通知 Lambda が購読するので管理者に届く。Lambda の失敗としても通知すると 2 通になる。
14
+ * target が alerts なので alerts には記録されず、ループしない)。接続などの想定外の例外は、そのまま Lambda の
15
+ * 失敗になる。
16
+ */
17
+ import { type RecordsClientOptions, type RecordsCollection } from '../client/recordsClient.js';
18
+ import { type AlertsCleanupCollection, type CleanupAlertsResult } from './alerts.js';
19
+ /**
20
+ * 1 回の実行で消す上限の既定。1 日 1 回の実行で、1 日に増える数より十分大きくする
21
+ * (指紋ごとに 1 件なので、通常は数件〜数十件。50 件 × 10 ページ = 読み・削除が 20 回で、時間切れにならない)
22
+ */
23
+ export declare const ALERTS_CLEANUP_MAX_PER_RUN = 500;
24
+ /** 削除の結果のログの出力先。fields には app・target(alerts)が入る */
25
+ export type AlertsCleanupLog = (level: 'info' | 'warn' | 'error', fields: Record<string, unknown>) => void;
26
+ export interface RunAlertsCleanupOptions {
27
+ /** アプリ名(ログの app) */
28
+ app: string;
29
+ /** 保存期間(日)。既定 ALERT_RETENTION_DAYS(28) */
30
+ olderThanDays?: number;
31
+ /** 1 回の実行で消す上限。既定 ALERTS_CLEANUP_MAX_PER_RUN(500) */
32
+ maxPerRun?: number;
33
+ /** ログの出力先(既定は logJson。app と target: alerts を付ける) */
34
+ log?: AlertsCleanupLog;
35
+ /** 現在時刻(テスト用) */
36
+ now?: () => Date;
37
+ }
38
+ /** 削除を 1 回行い、結果を構造化ログに出す(失敗は error、上限で止めたら warn、それ以外は info) */
39
+ export declare function runAlertsCleanup(collection: AlertsCleanupCollection, options: RunAlertsCleanupOptions): Promise<CleanupAlertsResult>;
40
+ /** 削除で読む alerts の行(使う項目だけ) */
41
+ export type AlertsCleanupRow = {
42
+ id: string;
43
+ lastSeenAt?: unknown;
44
+ [key: string]: unknown;
45
+ };
46
+ export interface AlertsCleanupHandlerOptions extends Omit<RunAlertsCleanupOptions, 'app'> {
47
+ /** アプリ名。省略時は環境変数 PROJECT_NAME */
48
+ app?: string;
49
+ /** Records のクライアント。url / region は省略時に環境変数。timeout は DynamoClient の既定 */
50
+ records?: RecordsClientOptions;
51
+ /**
52
+ * 削除で使う形を作る(既定は生のコレクションをそのまま使う)。読み取りを権限チェック層(astro の
53
+ * authorizedCollection など)に通すアプリは、受け取った生のコレクションを自分の層で包んで返す
54
+ */
55
+ collection?: (raw: RecordsCollection<AlertsCleanupRow>) => AlertsCleanupCollection;
56
+ }
57
+ /**
58
+ * 削除の定時 Lambda のハンドラを作る。アプリのエントリは、これを export するだけ:
59
+ * `export const handler = createAlertsCleanupHandler();`
60
+ * 呼び出しごとに Records(IAM 版 Function URL)に接続し、終わったら閉じる(`withCollection`)。
61
+ */
62
+ export declare function createAlertsCleanupHandler(options?: AlertsCleanupHandlerOptions): () => Promise<CleanupAlertsResult>;
63
+ //# sourceMappingURL=alerts-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerts-cleanup.d.ts","sourceRoot":"","sources":["../../src/notify/alerts-cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAEvB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAGL,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EAEzB,MAAM,aAAa,CAAC;AAGrB;;;GAGG;AACH,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,mDAAmD;AACnD,MAAM,MAAM,gBAAgB,GAAG,CAC7B,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,IAAI,CAAC;AAEV,MAAM,WAAW,uBAAuB;IACtC,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,iBAAiB;IACjB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,+DAA+D;AAC/D,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,uBAAuB,EACnC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CA+B9B;AAED,8BAA8B;AAC9B,MAAM,MAAM,gBAAgB,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE5F,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CAAC,uBAAuB,EAAE,KAAK,CAAC;IACvF,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,KAAK,uBAAuB,CAAC;CACpF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC,GACxC,MAAM,OAAO,CAAC,mBAAmB,CAAC,CAapC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * alerts の定時削除 Lambda のハンドラのファクトリ(ADR 0006 の (e)。ADR 0003 の保存期間)。
3
+ *
4
+ * 運用通知の記録(alerts)のうち、最後の発生から保存期間(既定 28 日)を過ぎたものを消す
5
+ * (ネイティブ TTL は効かない)。削除そのものは `cleanupAlerts`(変えない)。ここは、その周りの定型
6
+ * (1 回の上限、結果の構造化ログ、環境変数の確認、Records への接続と close)を持つ。docs/NOTIFY.md
7
+ *
8
+ * 設定は、オプションが環境変数より優先される(どちらも呼び出しのたびに読む):
9
+ * - `app`: PROJECT_NAME(必須)
10
+ * - `records.url` / `records.region`: RECORDS_IAM_URL(必須)/ AWS_REGION
11
+ *
12
+ * 失敗の扱い: 削除の失敗は握りつぶさず、構造化ログ(error。target は alerts)に出す。例外にはしない
13
+ * (このロググループは通知 Lambda が購読するので管理者に届く。Lambda の失敗としても通知すると 2 通になる。
14
+ * target が alerts なので alerts には記録されず、ループしない)。接続などの想定外の例外は、そのまま Lambda の
15
+ * 失敗になる。
16
+ */
17
+ import { withCollection, } from '../client/recordsClient.js';
18
+ import { ALERTS_RESOURCE, ALERT_RETENTION_DAYS, cleanupAlerts, } from './alerts.js';
19
+ import { logJson } from './logger.js';
20
+ /**
21
+ * 1 回の実行で消す上限の既定。1 日 1 回の実行で、1 日に増える数より十分大きくする
22
+ * (指紋ごとに 1 件なので、通常は数件〜数十件。50 件 × 10 ページ = 読み・削除が 20 回で、時間切れにならない)
23
+ */
24
+ export const ALERTS_CLEANUP_MAX_PER_RUN = 500;
25
+ /** 削除を 1 回行い、結果を構造化ログに出す(失敗は error、上限で止めたら warn、それ以外は info) */
26
+ export async function runAlertsCleanup(collection, options) {
27
+ const maxPerRun = options.maxPerRun ?? ALERTS_CLEANUP_MAX_PER_RUN;
28
+ const log = options.log ??
29
+ ((level, fields) => logJson(level, { app: options.app, target: ALERTS_RESOURCE, ...fields }));
30
+ const r = await cleanupAlerts({
31
+ collection,
32
+ olderThanDays: options.olderThanDays ?? ALERT_RETENTION_DAYS,
33
+ maxPerRun,
34
+ now: options.now,
35
+ });
36
+ const summary = { deleted: r.deleted, failed: r.failed, more: r.more };
37
+ if (r.errors.length > 0) {
38
+ log('error', {
39
+ event: 'ALERTS_CLEANUP_FAILED',
40
+ message: `alerts の削除に失敗しました: ${r.errors.join(' / ')}`.slice(0, 1000),
41
+ action: '翌日の定時実行で再試行されます。続く場合は Records Lambda のログを確認してください',
42
+ ...summary,
43
+ });
44
+ }
45
+ else if (r.more) {
46
+ log('warn', {
47
+ event: 'ALERTS_CLEANUP_BACKLOG',
48
+ message: `1 回の上限(${maxPerRun} 件)まで消しました。残りは翌日に消します`,
49
+ action: '毎日続く場合は、通知の発生源(同じ指紋で増え続けていないか)を確認してください',
50
+ ...summary,
51
+ });
52
+ }
53
+ else {
54
+ log('info', { message: 'alerts cleanup completed', ...summary });
55
+ }
56
+ return r;
57
+ }
58
+ /**
59
+ * 削除の定時 Lambda のハンドラを作る。アプリのエントリは、これを export するだけ:
60
+ * `export const handler = createAlertsCleanupHandler();`
61
+ * 呼び出しごとに Records(IAM 版 Function URL)に接続し、終わったら閉じる(`withCollection`)。
62
+ */
63
+ export function createAlertsCleanupHandler(options = {}) {
64
+ const { app: appOption, records, collection: wrap, ...run } = options;
65
+ return async () => {
66
+ const app = appOption ?? (process.env.PROJECT_NAME || undefined);
67
+ if (!app)
68
+ throw new Error('PROJECT_NAME environment variable is required');
69
+ const url = records?.url ?? (process.env.RECORDS_IAM_URL || undefined);
70
+ if (!url)
71
+ throw new Error('RECORDS_IAM_URL environment variable is required');
72
+ return withCollection(ALERTS_RESOURCE, (raw) => runAlertsCleanup(wrap ? wrap(raw) : raw, { ...run, app }), { ...records, url });
73
+ };
74
+ }
75
+ //# sourceMappingURL=alerts-cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alerts-cleanup.js","sourceRoot":"","sources":["../../src/notify/alerts-cleanup.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,8 @@
7
7
  export { createAdminDirectory, emailsOf, normalizeEmail } from './admins.js';
8
8
  export type { AdminDirectoryOptions, AdminRecord, AdminsLister } from './admins.js';
9
9
  export { ALERTS_RESOURCE, ALERT_RETENTION_DAYS, alertKeyOf, cleanupAlerts, createAlertRecorder, } from './alerts.js';
10
+ export { ALERTS_CLEANUP_MAX_PER_RUN, createAlertsCleanupHandler, runAlertsCleanup, } from './alerts-cleanup.js';
11
+ export type { AlertsCleanupHandlerOptions, AlertsCleanupLog, AlertsCleanupRow, RunAlertsCleanupOptions, } from './alerts-cleanup.js';
10
12
  export type { AlertInput, AlertOccurrence, AlertRecord, AlertRecorder, AlertRecorderOptions, AlertsCleanupCollection, AlertsCollection, CleanupAlertsOptions, CleanupAlertsResult, } from './alerts.js';
11
13
  export { createEmailChannel, sesSender } from './channels/email.js';
12
14
  export type { EmailMessage, EmailOptions, EmailSender, SesSendInput } from './channels/email.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,UAAU,EACV,aAAa,EACb,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,EACR,OAAO,GACR,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACjF,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EACV,cAAc,EACd,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,OAAO,EACP,MAAM,EACN,WAAW,EACX,MAAM,EACN,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,UAAU,EACV,aAAa,EACb,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,2BAA2B,EAC3B,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,EACR,OAAO,GACR,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACjF,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EACV,cAAc,EACd,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,OAAO,EACP,MAAM,EACN,WAAW,EACX,MAAM,EACN,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
@@ -6,6 +6,7 @@
6
6
  */
7
7
  export { createAdminDirectory, emailsOf, normalizeEmail } from './admins.js';
8
8
  export { ALERTS_RESOURCE, ALERT_RETENTION_DAYS, alertKeyOf, cleanupAlerts, createAlertRecorder, } from './alerts.js';
9
+ export { ALERTS_CLEANUP_MAX_PER_RUN, createAlertsCleanupHandler, runAlertsCleanup, } from './alerts-cleanup.js';
9
10
  export { createEmailChannel, sesSender } from './channels/email.js';
10
11
  export { createSlackChannel } from './channels/slack.js';
11
12
  export { createNotifyHandler } from './handler.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,UAAU,EACV,aAAa,EACb,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAYrB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,EACR,OAAO,GACR,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,UAAU,EACV,aAAa,EACb,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAkB7B,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,qBAAqB,EACrB,QAAQ,EACR,OAAO,GACR,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.17.0
2
- // Built: 2026-10-24T03:15:07.224Z
1
+ // @exabugs/dynamodb-client v1.18.0
2
+ // Built: 2026-10-24T04:27:35.011Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -38980,7 +38980,7 @@ async function handler(event) {
38980
38980
  return createCorsResponse(HTTP_STATUS.OK);
38981
38981
  }
38982
38982
  if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
38983
- const version = "1.17.0";
38983
+ const version = "1.18.0";
38984
38984
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
38985
38985
  }
38986
38986
  if (event.requestContext.http.method !== "POST") {
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.17.0
2
- // Built: 2026-10-24T03:15:07.224Z
1
+ // @exabugs/dynamodb-client v1.18.0
2
+ // Built: 2026-10-24T04:27:35.011Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.17.0
2
- // Built: 2026-10-24T03:15:07.224Z
1
+ // @exabugs/dynamodb-client v1.18.0
2
+ // Built: 2026-10-24T04:27:35.011Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.17.0
2
- // Built: 2026-10-24T03:15:07.224Z
1
+ // @exabugs/dynamodb-client v1.18.0
2
+ // Built: 2026-10-24T04:27:35.011Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
package/docs/NOTIFY.md CHANGED
@@ -181,25 +181,34 @@ export const handler = createNotifyHandler({
181
181
 
182
182
  ### 削除(定時 Lambda)
183
183
 
184
- ネイティブ TTL は効かない(`ttl` が最上位の属性にならない)。アプリの定時 Lambda(1 日 1 回など)から呼ぶ:
184
+ ネイティブ TTL は効かない(`ttl` が最上位の属性にならない)。アプリの定時 Lambda(EventBridge で 1 日 1 回など)のエントリは、ハンドラのファクトリを export するだけ(1.18.0。ADR 0006 の (e)):
185
185
 
186
186
  ```ts
187
- import { DynamoClient } from '@exabugs/dynamodb-client/client/iam';
188
- import { cleanupAlerts } from '@exabugs/dynamodb-client/notify';
189
-
190
- export const handler = async () => {
191
- const client = new DynamoClient(process.env.RECORDS_IAM_URL!, { auth: { region } });
192
- await client.connect();
193
- try {
194
- const r = await cleanupAlerts({ collection: client.db().collection('alerts'), maxPerRun: 500 });
195
- if (r.errors.length > 0) throw new Error(`alerts の削除に失敗: ${r.errors.join(' / ')}`);
196
- return r; // more: true なら、次回に残りを消す
197
- } finally {
198
- await client.close();
199
- }
200
- };
187
+ import { createAlertsCleanupHandler } from '@exabugs/dynamodb-client/notify';
188
+
189
+ // 環境変数: PROJECT_NAME(ログの app)/ RECORDS_IAM_URL(必須)/ AWS_REGION
190
+ export const handler = createAlertsCleanupHandler();
201
191
  ```
202
192
 
193
+ - 呼び出しごとに `withCollection('alerts', …)` で Records に接続し、終わったら閉じる。`runAlertsCleanup` → `cleanupAlerts` を 1 回行う
194
+ - 結果は構造化ログ(`logJson`。`app` と `target: 'alerts'` が付く)に出す: 失敗は error(`ALERTS_CLEANUP_FAILED`)、上限で止めたら warn(`ALERTS_CLEANUP_BACKLOG`)、それ以外は info(`alerts cleanup completed`)。**削除の失敗は例外にしない**(このロググループを通知 Lambda が購読していれば管理者に届く。Lambda の失敗としても通知すると 2 通になる。target が alerts なので alerts には記録されず、ループしない)。接続などの想定外の例外は、そのまま Lambda の失敗になる
195
+ - オプション(どれも任意。環境変数より優先):
196
+
197
+ | オプション | 既定 | 内容 |
198
+ | --------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
199
+ | `app` | `PROJECT_NAME` | ログの `app` |
200
+ | `records` | `RECORDS_IAM_URL` / `AWS_REGION` | Records のクライアント(`url` / `region` / `timeout`。timeout は `DynamoClient` の既定) |
201
+ | `collection` | 生のコレクション | `(raw) => AlertsCleanupCollection`。読み取りを権限チェック層(astro の `authorizedCollection` など)に通すアプリは、受け取った生のコレクションを包んで返す |
202
+ | `olderThanDays` | `ALERT_RETENTION_DAYS`(28) | 保存期間(日)。`createAlertRecorder` の `retentionDays` と揃える |
203
+ | `maxPerRun` | `ALERTS_CLEANUP_MAX_PER_RUN`(500) | 1 回の実行で消す上限(残りは次回) |
204
+ | `log` | `logJson`(`app`・`target` 付き) | `(level, fields) => void` |
205
+ | `now` | 現在時刻 | テスト用 |
206
+
207
+ - 削除とログだけを使うとき(自前で接続する・別の入口から呼ぶ)は `runAlertsCleanup(collection, { app, olderThanDays?, maxPerRun?, log?, now? })`。削除だけなら `cleanupAlerts({ collection, olderThanDays?, maxPerRun?, now? })`(結果を返すだけで、ログは出さない)
208
+ - IAM: Records の IAM URL の呼び出し(`lambda:InvokeFunctionUrl` と `lambda:InvokeFunction`)だけ。DynamoDB の権限は要らない
209
+
210
+ `cleanupAlerts` の動き:
211
+
203
212
  - `lastSeenAt` の昇順に条件なしで読み、期限内のものが出たら止める(`lastSeenAt: { $lt }` の範囲検索は、本体の不具合で `ValidationException` になるため。TODO.md)
204
213
  - 読んだ ID は直ちに `deleteMany({ id: { $in } })` で消す(本体とシャドーを一緒に消す)。存在しない ID の削除は Records が warn を出す(それも通知に戻る)
205
214
  - 失敗は `errors` / `failed` で返す(握りつぶさない)
@@ -9,7 +9,7 @@ Lambda などのサーバー側から Records(IAM 版 Function URL)を呼ぶ
9
9
  | `lazyCollection(name, options?)` | 最初の使用時に 1 回だけ接続し、コンテナ内で再利用。失敗はキャッシュしない。`.reset()` で破棄 |
10
10
  | `createAdminDirectory(options?)` | `@exabugs/dynamodb-client/notify`。`admins` の全件から通知の宛先(メール)を返す |
11
11
 
12
- システム変更履歴の記録 Lambda(`createSystemChangeHandler`)も、内部で `withCollection` を使う([SYSTEM_CHANGES.md](SYSTEM_CHANGES.md))。
12
+ システム変更履歴の記録 Lambda(`createSystemChangeHandler`。[SYSTEM_CHANGES.md](SYSTEM_CHANGES.md))と alerts の定時削除 Lambda(`createAlertsCleanupHandler`。[NOTIFY.md](NOTIFY.md))も、内部で `withCollection` を使う。
13
13
 
14
14
  `options`: `url`(省略時は環境変数 `RECORDS_IAM_URL`)、`region`(省略時は `AWS_REGION`、無ければ `REGION`)、`timeout`(ms。Records の Lambda の timeout より短くする)。
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exabugs/dynamodb-client",
3
- "version": "1.17.0",
3
+ "version": "1.18.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",