@exabugs/dynamodb-client 1.15.0 → 1.16.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,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.16.0] - 2026-10-20
11
+
12
+ ### Added
13
+
14
+ - **Records クライアントのヘルパー**(`@exabugs/dynamodb-client/client/iam`。ADR 0006 の段階 1): `createRecordsClient`(`DynamoClient` の生成と `connect()`)、`withCollection`(作る → 使う → 確実に close)、`lazyCollection`(初回の使用時に 1 回だけ接続して再利用)。`url` / `region` は省略時に環境変数 `RECORDS_IAM_URL` / `AWS_REGION` から読む。使い方は `docs/RECORDS_CLIENT.md`
15
+ - **`createAdminDirectory`**(`@exabugs/dynamodb-client/notify`): `admins` の全件から通知の宛先を返す `AdminDirectory`。宛先のフィールドは `recipientFields`(既定 `['id']`。`['email','id']` で `email ?? id`)。あわせて `normalizeEmail` / `emailsOf` を公開
16
+
17
+ ### Changed
18
+
19
+ - media の Records クライアント(`src/server/media/recordsClient.ts`)は、公開ヘルパー `lazyCollection` を使う。挙動は同じ(`connect()`、timeout 15 秒、`RECORDS_IAM_URL` の扱い、コールドスタートで 1 回の接続と再利用)。並行した初回の呼び出しが 1 つの接続を共有するようになった
20
+
21
+ ### Removed
22
+
23
+ - **terraform/media**: 変数 `table_name`、`table_arn`、`shadow_created_at_field`、`shadow_updated_at_field`、`shadow_string_max_bytes`、`shadow_number_padding`(ADR 0005、issue #94。1.15.0 で deprecated)。利用側が渡していると、`terraform plan` が「宣言されていない変数」で失敗する。移行: 渡すのをやめる(1.15.0 以降、渡しても無視されていた)
24
+
10
25
  ## [1.15.0] - 2026-10-18
11
26
 
12
27
  ### Added
package/README.md CHANGED
@@ -196,6 +196,7 @@ const article = await articles.findOne({ title: 'Hello DynamoDB' });
196
196
  - **[React Admin Integration](docs/react-admin-integration.md)** - Admin UI setup and many-to-many relationships
197
197
  - **[Authorization & Secure Setup](docs/AUTHORIZATION.md)** - 認可モデルと安全な構成(必読。JWT の有効性しか検証しない/管理者専用ユーザープール/`aud` 検証)
198
198
  - **[疎な検索の警告と対応](docs/SCAN_WARNING.md)** - 検索を打ち切らず、警告(SPARSE_SCAN)で気づいてインデックス・スキーマ・ロジックを見直す
199
+ - **[Records クライアントのヘルパー](docs/RECORDS_CLIENT.md)** - Lambda から Records を呼ぶ定型(`createRecordsClient` / `withCollection` / `lazyCollection`)
199
200
  - **[運用通知](docs/NOTIFY.md)** - ログ・アラームを、Slack・メールで管理者へ通知する部品(`@exabugs/dynamodb-client/notify`)
200
201
  - **[Deployment](docs/DEPLOYMENT.md)** - Production deployment guide
201
202
  - **[Terraform Modules](terraform/README.md)** - Infrastructure as Code
@@ -31,4 +31,5 @@ export interface ClientOptions extends Omit<BaseClientOptions<IAMAuthOptions>, '
31
31
  export declare class DynamoClient extends BaseDynamoClient<IAMAuthOptions> {
32
32
  constructor(endpoint: string, options: ClientOptions);
33
33
  }
34
+ export { createRecordsClient, lazyCollection, withCollection, type LazyCollection, type RecordsClientOptions, type RecordsCollection, } from './recordsClient.js';
34
35
  //# sourceMappingURL=index.iam.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.iam.d.ts","sourceRoot":"","sources":["../../src/client/index.iam.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,KAAK,aAAa,IAAI,iBAAiB,EACvC,YAAY,IAAI,gBAAgB,EACjC,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACpF,kBAAkB;IAClB,IAAI,EAAE,cAAc,CAAC;CACtB;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,gBAAgB,CAAC,cAAc,CAAC;gBACpD,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa;CAQrD"}
1
+ {"version":3,"file":"index.iam.d.ts","sourceRoot":"","sources":["../../src/client/index.iam.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,KAAK,aAAa,IAAI,iBAAiB,EACvC,YAAY,IAAI,gBAAgB,EACjC,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACpF,kBAAkB;IAClB,IAAI,EAAE,cAAc,CAAC;CACtB;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,gBAAgB,CAAC,cAAc,CAAC;gBACpD,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa;CAQrD;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,GACvB,MAAM,oBAAoB,CAAC"}
@@ -25,4 +25,5 @@ export class DynamoClient extends BaseDynamoClient {
25
25
  });
26
26
  }
27
27
  }
28
+ export { createRecordsClient, lazyCollection, withCollection, } from './recordsClient.js';
28
29
  //# sourceMappingURL=index.iam.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.iam.js","sourceRoot":"","sources":["../../src/client/index.iam.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAEL,YAAY,IAAI,gBAAgB,GACjC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAmC,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAkB7C;;;;;GAKG;AACH,MAAM,OAAO,YAAa,SAAQ,gBAAgC;IAChE,YAAY,QAAgB,EAAE,OAAsB;QAClD,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,GAAW,EAAE,IAAY,EAAE,IAAqB,EAAE,EAAE;YAClF,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACnD,CAAC;YACD,OAAO,MAAM,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
1
+ {"version":3,"file":"index.iam.js","sourceRoot":"","sources":["../../src/client/index.iam.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAEL,YAAY,IAAI,gBAAgB,GACjC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAmC,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAkB7C;;;;;GAKG;AACH,MAAM,OAAO,YAAa,SAAQ,gBAAgC;IAChE,YAAY,QAAgB,EAAE,OAAsB;QAClD,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,GAAW,EAAE,IAAY,EAAE,IAAqB,EAAE,EAAE;YAClF,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACnD,CAAC;YACD,OAAO,MAAM,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAED,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,cAAc,GAIf,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Records(IAM 版 Function URL)を呼ぶクライアントのヘルパー(ADR 0006)
3
+ *
4
+ * Lambda などのサーバー側が Records のコレクションを使うときの定型を持つ。
5
+ * - `createRecordsClient`: `DynamoClient` の生成と `connect()`(`connect()` 忘れの防止)
6
+ * - `withCollection`: 作る → 使う → 確実に close(Lambda の 1 回の呼び出しで使い切る用途)
7
+ * - `lazyCollection`: 初回の使用時に 1 回だけ接続して再利用(コールドスタートで共有する用途)
8
+ *
9
+ * `url` と `region` は、省略時に環境変数 `RECORDS_IAM_URL`、`AWS_REGION`(無ければ `REGION`)から読む。
10
+ */
11
+ import { DynamoClient } from './index.iam.js';
12
+ import type { Collection } from './index.iam.js';
13
+ /** Records 用のコレクションの型(IAM 認証) */
14
+ export type RecordsCollection<T extends {
15
+ id: string;
16
+ [key: string]: unknown;
17
+ }> = Collection<T, {
18
+ region: string;
19
+ }>;
20
+ export interface RecordsClientOptions {
21
+ /** Records の IAM 版 Function URL。省略時は環境変数 RECORDS_IAM_URL */
22
+ url?: string;
23
+ /** AWS リージョン。省略時は環境変数 AWS_REGION(無ければ REGION) */
24
+ region?: string;
25
+ /** リクエストの timeout(ms)。省略時は DynamoClient の既定。Records の Lambda の timeout より短くする */
26
+ timeout?: number;
27
+ }
28
+ type RecordShape = {
29
+ id: string;
30
+ [key: string]: unknown;
31
+ };
32
+ /** `DynamoClient` を生成して接続する。IAM では接続は状態の更新だけ(`db()` の前に必須) */
33
+ export declare function createRecordsClient(options?: RecordsClientOptions): Promise<DynamoClient>;
34
+ /**
35
+ * 接続して、コレクションで `fn` を実行し、成功でも失敗でも close する。
36
+ * `fn` の例外はそのまま投げる(close の失敗で `fn` の例外を隠さない)。
37
+ */
38
+ export declare function withCollection<T extends RecordShape = RecordShape, R = unknown>(name: string, fn: (collection: RecordsCollection<T>) => Promise<R>, options?: RecordsClientOptions): Promise<R>;
39
+ /** `lazyCollection` の戻り値。呼ぶとコレクションを返す(初回に接続) */
40
+ export interface LazyCollection<T extends RecordShape> {
41
+ (): Promise<RecordsCollection<T>>;
42
+ /** キャッシュを捨てる(テスト用。次の呼び出しで接続し直す) */
43
+ reset(): void;
44
+ }
45
+ /**
46
+ * 最初の使用時に接続し、以降は同じコレクションを再利用する(コールドスタートで 1 回だけ接続)。
47
+ * 並行した初回の呼び出しは 1 つの接続を共有する。失敗したらキャッシュせず、次の呼び出しでやり直す。
48
+ */
49
+ export declare function lazyCollection<T extends RecordShape>(name: string, options?: RecordsClientOptions): LazyCollection<T>;
50
+ export {};
51
+ //# sourceMappingURL=recordsClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordsClient.d.ts","sourceRoot":"","sources":["../../src/client/recordsClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,iCAAiC;AACjC,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,IAAI,UAAU,CAC1F,CAAC,EACD;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CACnB,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE1D,8DAA8D;AAC9D,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,YAAY,CAAC,CAevB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,CAAC,GAAG,OAAO,EACnF,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EACpD,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,gDAAgD;AAChD,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,WAAW;IACnD,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,mCAAmC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAClD,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,cAAc,CAAC,CAAC,CAAC,CAkBnB"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Records(IAM 版 Function URL)を呼ぶクライアントのヘルパー(ADR 0006)
3
+ *
4
+ * Lambda などのサーバー側が Records のコレクションを使うときの定型を持つ。
5
+ * - `createRecordsClient`: `DynamoClient` の生成と `connect()`(`connect()` 忘れの防止)
6
+ * - `withCollection`: 作る → 使う → 確実に close(Lambda の 1 回の呼び出しで使い切る用途)
7
+ * - `lazyCollection`: 初回の使用時に 1 回だけ接続して再利用(コールドスタートで共有する用途)
8
+ *
9
+ * `url` と `region` は、省略時に環境変数 `RECORDS_IAM_URL`、`AWS_REGION`(無ければ `REGION`)から読む。
10
+ */
11
+ import { DynamoClient } from './index.iam.js';
12
+ /** `DynamoClient` を生成して接続する。IAM では接続は状態の更新だけ(`db()` の前に必須) */
13
+ export async function createRecordsClient(options = {}) {
14
+ const url = options.url ?? process.env.RECORDS_IAM_URL;
15
+ if (!url) {
16
+ throw new Error('RECORDS_IAM_URL environment variable is not set');
17
+ }
18
+ const region = options.region ?? process.env.AWS_REGION ?? process.env.REGION;
19
+ if (!region) {
20
+ throw new Error('AWS_REGION environment variable is not set');
21
+ }
22
+ const client = new DynamoClient(url, {
23
+ auth: { region },
24
+ ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
25
+ });
26
+ await client.connect();
27
+ return client;
28
+ }
29
+ /**
30
+ * 接続して、コレクションで `fn` を実行し、成功でも失敗でも close する。
31
+ * `fn` の例外はそのまま投げる(close の失敗で `fn` の例外を隠さない)。
32
+ */
33
+ export async function withCollection(name, fn, options = {}) {
34
+ const client = await createRecordsClient(options);
35
+ try {
36
+ return await fn(client.db().collection(name));
37
+ }
38
+ finally {
39
+ await client.close();
40
+ }
41
+ }
42
+ /**
43
+ * 最初の使用時に接続し、以降は同じコレクションを再利用する(コールドスタートで 1 回だけ接続)。
44
+ * 並行した初回の呼び出しは 1 つの接続を共有する。失敗したらキャッシュせず、次の呼び出しでやり直す。
45
+ */
46
+ export function lazyCollection(name, options = {}) {
47
+ let cached;
48
+ const get = (() => {
49
+ if (!cached) {
50
+ const attempt = createRecordsClient(options).then((client) => client.db().collection(name));
51
+ cached = attempt;
52
+ attempt.catch(() => {
53
+ if (cached === attempt)
54
+ cached = undefined;
55
+ });
56
+ }
57
+ return cached;
58
+ });
59
+ get.reset = () => {
60
+ cached = undefined;
61
+ };
62
+ return get;
63
+ }
64
+ //# sourceMappingURL=recordsClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordsClient.js","sourceRoot":"","sources":["../../src/client/recordsClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAoB9C,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,UAAgC,EAAE;IAElC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IACvD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;IAC9E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,GAAG,EAAE;QACnC,IAAI,EAAE,EAAE,MAAM,EAAE;QAChB,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvE,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;IACvB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAY,EACZ,EAAoD,EACpD,UAAgC,EAAE;IAElC,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,UAAU,CAAI,IAAI,CAAC,CAAC,CAAC;IACnD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AASD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,UAAgC,EAAE;IAElC,IAAI,MAAiD,CAAC;IACtD,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE;QAChB,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAC3D,MAAM,CAAC,EAAE,EAAE,CAAC,UAAU,CAAI,IAAI,CAAC,CAChC,CAAC;YACF,MAAM,GAAG,OAAO,CAAC;YACjB,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE;gBACjB,IAAI,MAAM,KAAK,OAAO;oBAAE,MAAM,GAAG,SAAS,CAAC;YAC7C,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAsB,CAAC;IACxB,GAAG,CAAC,KAAK,GAAG,GAAG,EAAE;QACf,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC,CAAC;IACF,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { RecordsClientOptions } from '../client/recordsClient.js';
2
+ import type { AdminDirectory } from './types.js';
3
+ export interface AdminRecord {
4
+ id: string;
5
+ [key: string]: unknown;
6
+ }
7
+ /**
8
+ * 管理者の一覧を読む側(`authorizedCollection` の `list` と同じ形)。
9
+ * 自前で接続せず、呼び出し側が権限チェック層などを通した読み取りを渡すときに使う。
10
+ * `all.maxItems` を超える件数のときは、呼び出し側が例外にする(黙って切らない)
11
+ */
12
+ export interface AdminsLister {
13
+ list(filter: Record<string, unknown>, options?: {
14
+ sort?: Record<string, 'asc' | 'desc'>;
15
+ all?: {
16
+ maxItems: number;
17
+ };
18
+ }): Promise<readonly AdminRecord[]>;
19
+ }
20
+ export interface AdminDirectoryOptions extends RecordsClientOptions {
21
+ /** 管理者のコレクション名(自前で接続するとき)。既定 `admins` */
22
+ collection?: string;
23
+ /**
24
+ * 管理者を読む側。指定すると自前で接続せず(url / region / timeout / collection は使わない)、
25
+ * これを通して読む。astro は `authorizedCollection` を渡す
26
+ */
27
+ source?: AdminsLister;
28
+ /** 管理者の数の上限。件数が上限を超えたら例外(ちょうど上限は成功)。通知先の設計を見直す。既定 200 */
29
+ maxAdmins?: number;
30
+ /**
31
+ * 宛先を取り出すフィールド。先頭から見て、最初に null / undefined でない値を使う。
32
+ * 既定 `['id']`(asanowa)。astro の現行は `['email', 'id']`
33
+ */
34
+ recipientFields?: readonly string[];
35
+ /** true なら id の昇順で読み、宛先の順序を固定する(astro の現行)。既定 false(順序は不定) */
36
+ sortById?: boolean;
37
+ }
38
+ /** メールアドレスの形式なら小文字・前後空白除去で返す。そうでなければ null */
39
+ export declare function normalizeEmail(value: unknown): string | null;
40
+ /** 管理者のレコード → 宛先(小文字・重複なし・不正な形式は除く)。id がメールアドレス */
41
+ export declare function emailsOf(admins: readonly AdminRecord[], fields?: readonly string[]): string[];
42
+ /** 管理者の宛先を、呼び出しごとに Records から読む(`createNotifyHandler` の `admins` に渡す) */
43
+ export declare function createAdminDirectory(options?: AdminDirectoryOptions): AdminDirectory;
44
+ //# sourceMappingURL=admins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admins.d.ts","sourceRoot":"","sources":["../../src/notify/admins.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,CACF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC;QAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAC9E,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAI5D;AAED,mDAAmD;AACnD,wBAAgB,QAAQ,CACtB,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,MAAM,GAAE,SAAS,MAAM,EAAW,GACjC,MAAM,EAAE,CAQV;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAsCxF"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 通知の宛先(管理者全員)の取得(ADR 0006)
3
+ *
4
+ * 管理者は `admins` レコード(管理画面で増減)が正本。レコードの id が小文字のメールアドレス。
5
+ * Records の IAM 版 Function URL 経由で全件を読み、正規化したメールアドレスを返す。
6
+ */
7
+ import { withCollection } from '../client/recordsClient.js';
8
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
+ const PAGE_SIZE = 50;
10
+ /** メールアドレスの形式なら小文字・前後空白除去で返す。そうでなければ null */
11
+ export function normalizeEmail(value) {
12
+ if (typeof value !== 'string')
13
+ return null;
14
+ const email = value.trim().toLowerCase();
15
+ return EMAIL_PATTERN.test(email) ? email : null;
16
+ }
17
+ /** 管理者のレコード → 宛先(小文字・重複なし・不正な形式は除く)。id がメールアドレス */
18
+ export function emailsOf(admins, fields = ['id']) {
19
+ const set = new Set();
20
+ for (const a of admins) {
21
+ const key = fields.find((f) => a[f] !== undefined && a[f] !== null);
22
+ const email = normalizeEmail(key === undefined ? undefined : a[key]);
23
+ if (email)
24
+ set.add(email);
25
+ }
26
+ return [...set];
27
+ }
28
+ /** 管理者の宛先を、呼び出しごとに Records から読む(`createNotifyHandler` の `admins` に渡す) */
29
+ export function createAdminDirectory(options = {}) {
30
+ const { collection = 'admins', source, maxAdmins = 200, recipientFields = ['id'], sortById = false, ...clientOptions } = options;
31
+ const sort = sortById ? { id: 'asc' } : undefined;
32
+ return {
33
+ async listEmails() {
34
+ if (source) {
35
+ const all = await source.list({}, { ...(sort ? { sort } : {}), all: { maxItems: maxAdmins } });
36
+ return emailsOf(all, recipientFields);
37
+ }
38
+ return await withCollection(collection, async (admins) => {
39
+ const cursor = admins.find({});
40
+ if (sort)
41
+ cursor.sort(sort);
42
+ // 上限 + 1 件まで読めれば、超過を件数で判定できる(ちょうど上限は成功)
43
+ const all = await cursor.toArrayAll({
44
+ pageSize: PAGE_SIZE,
45
+ maxPages: Math.ceil((maxAdmins + 1) / PAGE_SIZE),
46
+ });
47
+ if (all.length > maxAdmins) {
48
+ throw new Error(`Too many admins: more than ${maxAdmins} in "${collection}"`);
49
+ }
50
+ return emailsOf(all, recipientFields);
51
+ }, clientOptions);
52
+ },
53
+ };
54
+ }
55
+ //# sourceMappingURL=admins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admins.js","sourceRoot":"","sources":["../../src/notify/admins.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@
4
4
  * ログ(warn 以上)・CloudWatch アラーム・直接呼び出しを Notice にそろえ、重複抑制のうえ、
5
5
  * Slack・メールなどのチャネルへ配信する部品。配線(購読フィルタ・Lambda・Terraform)は各アプリが行う。
6
6
  */
7
+ export { createAdminDirectory, emailsOf, normalizeEmail } from './admins.js';
8
+ export type { AdminDirectoryOptions, AdminRecord, AdminsLister } from './admins.js';
7
9
  export { ALERTS_RESOURCE, ALERT_RETENTION_DAYS, alertKeyOf, cleanupAlerts, createAlertRecorder, } from './alerts.js';
8
10
  export type { AlertInput, AlertOccurrence, AlertRecord, AlertRecorder, AlertRecorderOptions, AlertsCleanupCollection, AlertsCollection, CleanupAlertsOptions, CleanupAlertsResult, } from './alerts.js';
9
11
  export { createEmailChannel, sesSender } 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,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,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"}
@@ -4,6 +4,7 @@
4
4
  * ログ(warn 以上)・CloudWatch アラーム・直接呼び出しを Notice にそろえ、重複抑制のうえ、
5
5
  * Slack・メールなどのチャネルへ配信する部品。配線(購読フィルタ・Lambda・Terraform)は各アプリが行う。
6
6
  */
7
+ export { createAdminDirectory, emailsOf, normalizeEmail } from './admins.js';
7
8
  export { ALERTS_RESOURCE, ALERT_RETENTION_DAYS, alertKeyOf, cleanupAlerts, createAlertRecorder, } from './alerts.js';
8
9
  export { createEmailChannel, sesSender } from './channels/email.js';
9
10
  export { createSlackChannel } from './channels/slack.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,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;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,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.15.0
2
- // Built: 2026-10-18T10:11:26.658Z
1
+ // @exabugs/dynamodb-client v1.16.0
2
+ // Built: 2026-10-20T00:50:17.412Z
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.15.0";
38983
+ const version = "1.16.0";
38984
38984
  return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
38985
38985
  }
38986
38986
  if (event.requestContext.http.method !== "POST") {
@@ -1 +1 @@
1
- {"version":3,"file":"recordsClient.d.ts","sourceRoot":"","sources":["../../../src/server/media/recordsClient.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAiCjD,uCAAuC;AACvC,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAGtF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,CASf;AAED,8BAA8B;AAC9B,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
1
+ {"version":3,"file":"recordsClient.d.ts","sourceRoot":"","sources":["../../../src/server/media/recordsClient.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,QAAS,CAAC;AASjD,uCAAuC;AACvC,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAGtF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,CASf;AAED,8BAA8B;AAC9B,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
@@ -2,42 +2,22 @@
2
2
  * media の Lambda(process-handler / process-handler-dlq)が Records を呼ぶための共通部品
3
3
  *
4
4
  * DynamoDB には直接触れず、Records の IAM 版 Function URL を `DynamoClient`(SigV4 署名)で
5
- * 呼ぶ(詳細: docs/adr/0005-writes-through-records.md)。
5
+ * 呼ぶ(詳細: docs/adr/0005-writes-through-records.md)。接続の定型は公開ヘルパー
6
+ * `lazyCollection`(src/client/recordsClient.ts。ADR 0006)に任せる。
6
7
  * 失敗は握りつぶさず、そのまま例外にする(呼び出し側が再スローして、Lambda の非同期再試行と
7
8
  * DLQ に乗せる)。
8
9
  */
9
- import { DynamoClient } from '../../client/index.iam.js';
10
+ import { lazyCollection } from '../../client/recordsClient.js';
10
11
  /** Records へのリクエストの timeout(ms)。Records の Lambda の timeout(30 秒)より短くする */
11
12
  export const RECORDS_REQUEST_TIMEOUT_MS = 15_000;
12
13
  const MEDIA_COLLECTION = 'media';
13
- let mediaCollection;
14
- /**
15
- * コールドスタートで 1 回だけ生成・接続して再利用する。
16
- * `db()` は `connect()` の前に呼ぶと例外になるため、接続を待つ(IAM では接続は状態の更新だけ)。
17
- * 失敗したらキャッシュせず、次の呼び出しでやり直す。
18
- */
19
- async function getMediaCollection() {
20
- if (!mediaCollection) {
21
- const endpoint = process.env.RECORDS_IAM_URL;
22
- if (!endpoint) {
23
- throw new Error('RECORDS_IAM_URL environment variable is not set');
24
- }
25
- const region = process.env.AWS_REGION ?? process.env.REGION;
26
- if (!region) {
27
- throw new Error('AWS_REGION environment variable is not set');
28
- }
29
- const client = new DynamoClient(endpoint, {
30
- auth: { region },
31
- timeout: RECORDS_REQUEST_TIMEOUT_MS,
32
- });
33
- await client.connect();
34
- mediaCollection = client.db().collection(MEDIA_COLLECTION);
35
- }
36
- return mediaCollection;
37
- }
14
+ /** コールドスタートで 1 回だけ接続して再利用する。環境変数は初回の使用時に読む */
15
+ const mediaCollection = lazyCollection(MEDIA_COLLECTION, {
16
+ timeout: RECORDS_REQUEST_TIMEOUT_MS,
17
+ });
38
18
  /** media レコードを 1 件読む。無ければ undefined */
39
19
  export async function findMediaRecord(fileId) {
40
- const records = await (await getMediaCollection()).findMany([fileId]);
20
+ const records = await (await mediaCollection()).findMany([fileId]);
41
21
  return records[0];
42
22
  }
43
23
  /**
@@ -46,10 +26,10 @@ export async function findMediaRecord(fileId) {
46
26
  */
47
27
  export async function writeMediaRecord(fileId, fields) {
48
28
  const now = new Date().toISOString();
49
- await (await getMediaCollection()).updateOne({ id: fileId }, { $set: { ...fields, updatedAt: now }, $setOnInsert: { createdAt: now } }, { upsert: true });
29
+ await (await mediaCollection()).updateOne({ id: fileId }, { $set: { ...fields, updatedAt: now }, $setOnInsert: { createdAt: now } }, { upsert: true });
50
30
  }
51
31
  /** テスト用: キャッシュしたクライアントを捨てる */
52
32
  export function resetRecordsClientForTest() {
53
- mediaCollection = undefined;
33
+ mediaCollection.reset();
54
34
  }
55
35
  //# sourceMappingURL=recordsClient.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recordsClient.js","sourceRoot":"","sources":["../../../src/server/media/recordsClient.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAIzD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAEjD,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAIjC,IAAI,eAA4C,CAAC;AAEjD;;;;GAIG;AACH,KAAK,UAAU,kBAAkB;IAC/B,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;QAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;QAC5D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAChE,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,QAAQ,EAAE;YACxC,IAAI,EAAE,EAAE,MAAM,EAAE;YAChB,OAAO,EAAE,0BAA0B;SACpC,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,eAAe,GAAG,MAAM,CAAC,EAAE,EAAE,CAAC,UAAU,CAA+B,gBAAgB,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,uCAAuC;AACvC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,kBAAkB,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,MAA+B;IAE/B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,CACJ,MAAM,kBAAkB,EAAE,CAC3B,CAAC,SAAS,CACT,EAAE,EAAE,EAAE,MAAM,EAAE,EACd,EAAE,IAAI,EAAE,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EACzE,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;AACJ,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,yBAAyB;IACvC,eAAe,GAAG,SAAS,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"recordsClient.js","sourceRoot":"","sources":["../../../src/server/media/recordsClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAG/D,0EAA0E;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAEjD,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAEjC,8CAA8C;AAC9C,MAAM,eAAe,GAAG,cAAc,CAA+B,gBAAgB,EAAE;IACrF,OAAO,EAAE,0BAA0B;CACpC,CAAC,CAAC;AAEH,uCAAuC;AACvC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,eAAe,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACnE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,MAA+B;IAE/B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,CACJ,MAAM,eAAe,EAAE,CACxB,CAAC,SAAS,CACT,EAAE,EAAE,EAAE,MAAM,EAAE,EACd,EAAE,IAAI,EAAE,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EACzE,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;AACJ,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,yBAAyB;IACvC,eAAe,CAAC,KAAK,EAAE,CAAC;AAC1B,CAAC"}
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.15.0
2
- // Built: 2026-10-18T10:11:26.658Z
1
+ // @exabugs/dynamodb-client v1.16.0
2
+ // Built: 2026-10-20T00:50:17.412Z
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.15.0
2
- // Built: 2026-10-18T10:11:26.658Z
1
+ // @exabugs/dynamodb-client v1.16.0
2
+ // Built: 2026-10-20T00:50:17.412Z
3
3
  "use strict";
4
4
  var __create = Object.create;
5
5
  var __defProp = Object.defineProperty;
@@ -45859,38 +45859,59 @@ var DynamoClient2 = class extends DynamoClient {
45859
45859
  }
45860
45860
  };
45861
45861
 
45862
+ // src/client/recordsClient.ts
45863
+ async function createRecordsClient(options = {}) {
45864
+ const url = options.url ?? process.env.RECORDS_IAM_URL;
45865
+ if (!url) {
45866
+ throw new Error("RECORDS_IAM_URL environment variable is not set");
45867
+ }
45868
+ const region = options.region ?? process.env.AWS_REGION ?? process.env.REGION;
45869
+ if (!region) {
45870
+ throw new Error("AWS_REGION environment variable is not set");
45871
+ }
45872
+ const client = new DynamoClient2(url, {
45873
+ auth: { region },
45874
+ ...options.timeout !== void 0 ? { timeout: options.timeout } : {}
45875
+ });
45876
+ await client.connect();
45877
+ return client;
45878
+ }
45879
+ __name(createRecordsClient, "createRecordsClient");
45880
+ function lazyCollection(name, options = {}) {
45881
+ let cached;
45882
+ const get2 = /* @__PURE__ */ __name((() => {
45883
+ if (!cached) {
45884
+ const attempt = createRecordsClient(options).then(
45885
+ (client) => client.db().collection(name)
45886
+ );
45887
+ cached = attempt;
45888
+ attempt.catch(() => {
45889
+ if (cached === attempt) cached = void 0;
45890
+ });
45891
+ }
45892
+ return cached;
45893
+ }), "get");
45894
+ get2.reset = () => {
45895
+ cached = void 0;
45896
+ };
45897
+ return get2;
45898
+ }
45899
+ __name(lazyCollection, "lazyCollection");
45900
+
45862
45901
  // src/server/media/recordsClient.ts
45863
45902
  var RECORDS_REQUEST_TIMEOUT_MS = 15e3;
45864
45903
  var MEDIA_COLLECTION = "media";
45865
- var mediaCollection;
45866
- async function getMediaCollection() {
45867
- if (!mediaCollection) {
45868
- const endpoint = process.env.RECORDS_IAM_URL;
45869
- if (!endpoint) {
45870
- throw new Error("RECORDS_IAM_URL environment variable is not set");
45871
- }
45872
- const region = process.env.AWS_REGION ?? process.env.REGION;
45873
- if (!region) {
45874
- throw new Error("AWS_REGION environment variable is not set");
45875
- }
45876
- const client = new DynamoClient2(endpoint, {
45877
- auth: { region },
45878
- timeout: RECORDS_REQUEST_TIMEOUT_MS
45879
- });
45880
- await client.connect();
45881
- mediaCollection = client.db().collection(MEDIA_COLLECTION);
45882
- }
45883
- return mediaCollection;
45884
- }
45885
- __name(getMediaCollection, "getMediaCollection");
45904
+ var mediaCollection = lazyCollection(MEDIA_COLLECTION, {
45905
+ timeout: RECORDS_REQUEST_TIMEOUT_MS
45906
+ });
45886
45907
  async function findMediaRecord(fileId) {
45887
- const records = await (await getMediaCollection()).findMany([fileId]);
45908
+ const records = await (await mediaCollection()).findMany([fileId]);
45888
45909
  return records[0];
45889
45910
  }
45890
45911
  __name(findMediaRecord, "findMediaRecord");
45891
45912
  async function writeMediaRecord(fileId, fields) {
45892
45913
  const now = (/* @__PURE__ */ new Date()).toISOString();
45893
- await (await getMediaCollection()).updateOne(
45914
+ await (await mediaCollection()).updateOne(
45894
45915
  { id: fileId },
45895
45916
  { $set: { ...fields, updatedAt: now }, $setOnInsert: { createdAt: now } },
45896
45917
  { upsert: true }