@exabugs/dynamodb-client 1.14.1 → 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 +43 -0
- package/README.md +1 -0
- package/dist/client/index.iam.d.ts +1 -0
- package/dist/client/index.iam.d.ts.map +1 -1
- package/dist/client/index.iam.js +1 -0
- package/dist/client/index.iam.js.map +1 -1
- package/dist/client/recordsClient.d.ts +51 -0
- package/dist/client/recordsClient.d.ts.map +1 -0
- package/dist/client/recordsClient.js +64 -0
- package/dist/client/recordsClient.js.map +1 -0
- package/dist/notify/admins.d.ts +44 -0
- package/dist/notify/admins.d.ts.map +1 -0
- package/dist/notify/admins.js +55 -0
- package/dist/notify/admins.js.map +1 -0
- package/dist/notify/index.d.ts +2 -0
- package/dist/notify/index.d.ts.map +1 -1
- package/dist/notify/index.js +1 -0
- package/dist/notify/index.js.map +1 -1
- package/dist/server/handler.cjs +3 -3
- package/dist/server/media/process-handler-dlq.d.ts.map +1 -1
- package/dist/server/media/process-handler-dlq.js +8 -19
- package/dist/server/media/process-handler-dlq.js.map +1 -1
- package/dist/server/media/process-handler.d.ts.map +1 -1
- package/dist/server/media/process-handler.js +5 -18
- package/dist/server/media/process-handler.js.map +1 -1
- package/dist/server/media/recordsClient.d.ts +13 -0
- package/dist/server/media/recordsClient.d.ts.map +1 -0
- package/dist/server/media/recordsClient.js +35 -0
- package/dist/server/media/recordsClient.js.map +1 -0
- package/dist/server/media-handler.cjs +2 -2
- package/dist/server/media-process-handler-dlq.cjs +1157 -3606
- package/dist/server/media-process-handler-dlq.cjs.map +4 -4
- package/dist/server/media-process-handler.cjs +1211 -3675
- package/dist/server/media-process-handler.cjs.map +4 -4
- package/docs/DYNAMODB_ACCESS.md +17 -9
- package/docs/RECORDS_CLIENT.md +45 -0
- package/package.json +3 -2
- package/terraform/media/README.md +4 -2
- package/terraform/media/main.tf +26 -27
- package/terraform/media/variables.tf +21 -31
package/docs/DYNAMODB_ACCESS.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# DynamoDB へのアクセスのガードレール
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## アーキテクチャ(全文。他の文書は、これを要約して参照する)
|
|
4
4
|
|
|
5
|
-
dynamodb-client
|
|
6
|
-
|
|
5
|
+
**DynamoDB の操作は、dynamodb-client(Records)の 1 か所で行う。**
|
|
6
|
+
|
|
7
|
+
- クライアント(BFF、管理画面、media の Lambda、運用スクリプト)は、**ビジネスのリクエスト**を受け、dynamodb-client の API(Records)への操作に展開する。通常の操作の API は `DynamoClient`。**media のアップロードの処理も、ビジネスのリクエストの一つ**であり、BFF など他のクライアントと区別しない。
|
|
8
|
+
- DynamoDB の操作の制御(認可、監査、シャドーの維持、観測、容量)は、Records の **1 か所**で行う。全クライアントが Records に依存する、同じ構造である。
|
|
9
|
+
- **例外を作らない。** 例外(Lambda が本体テーブルを直接読み書きするなど)は、監査ログ(ADR 0004)のような横断機能の穴になる。直接アクセスは、ガードレール(ESLint と IAM の検査。許可リスト)で禁止する。
|
|
10
|
+
- **対象外**(別テーブル `notices` など)は、構造が違う内部用のテーブルで、ビジネスのデータではない。「例外」ではない。
|
|
11
|
+
|
|
12
|
+
### 書き方の規則
|
|
13
|
+
|
|
14
|
+
設計書・ADR では、Records への依存や HTTP 1 回分の遅延を、「限界」「問題」として書かない。**アーキテクチャの帰結(設計上の性質)**として書く。Records にリソースごとの認可がない、という既知の制約は、この構造とは独立に、別に書く([AUTHORIZATION.md](AUTHORIZATION.md))。
|
|
15
|
+
|
|
16
|
+
関連: [ADR 0005](adr/0005-writes-through-records.md)(書き込みを Records に通す。「0. アーキテクチャ(前提)」)。
|
|
7
17
|
|
|
8
18
|
## 検査
|
|
9
19
|
|
|
@@ -128,7 +138,7 @@ ESLint は、エディタでも違反をその場で示す。CLI は ESLint の
|
|
|
128
138
|
|
|
129
139
|
## 許可リストの種別と約束
|
|
130
140
|
|
|
131
|
-
- **恒久(permanent)**: 構造上必要な例外。このライブラリでは、Records の本体、Records が使う共有部品、別テーブル `notices`(`src/notify/store.ts
|
|
141
|
+
- **恒久(permanent)**: 構造上必要な例外。このライブラリでは、Records の本体、Records が使う共有部品、別テーブル `notices`(`src/notify/store.ts`)。`notices` は例外ではなく原則の**対象外**(構造が違う別テーブルで、通知の重複抑制・流量制限の内部用。ビジネスのデータではない)。検査が IAM をテーブルごとに区別できないため permanent として載せる(ADR 0005)
|
|
132
142
|
- **負債(debt)**: 期限つき。消す作業が決まっている
|
|
133
143
|
|
|
134
144
|
運用の約束:
|
|
@@ -145,8 +155,6 @@ ESLint は、エディタでも違反をその場で示す。CLI は ESLint の
|
|
|
145
155
|
|
|
146
156
|
## 現状の負債
|
|
147
157
|
|
|
148
|
-
| 場所
|
|
149
|
-
|
|
|
150
|
-
| `src/
|
|
151
|
-
| `terraform/media/main.tf` の `process_handler_dynamodb`、`process_handler_dlq_dynamodb` | media の Lambda に本体テーブルの権限 | 同上 |
|
|
152
|
-
| `src/dynamodb.ts` | 本体と重複した未使用のクライアント生成(テストからのみ参照) | `src/dynamodb.ts` と `__tests__/dynamodb.test.ts` の削除 |
|
|
158
|
+
| 場所 | 内容 | 消す作業 |
|
|
159
|
+
| ----------------- | ------------------------------------------------------------ | -------------------------------------------------------- |
|
|
160
|
+
| `src/dynamodb.ts` | 本体と重複した未使用のクライアント生成(テストからのみ参照) | `src/dynamodb.ts` と `__tests__/dynamodb.test.ts` の削除 |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Records クライアントのヘルパー
|
|
2
|
+
|
|
3
|
+
Lambda などのサーバー側から Records(IAM 版 Function URL)を呼ぶ定型を、ライブラリが持つ([ADR 0006](adr/0006-library-owns-table-operations.md))。`@exabugs/dynamodb-client/client/iam` から import する。
|
|
4
|
+
|
|
5
|
+
| 関数 | 用途 |
|
|
6
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `createRecordsClient(options?)` | `DynamoClient` の生成と `connect()`。`connect()` の呼び忘れを防ぐ |
|
|
8
|
+
| `withCollection(name, fn, options?)` | 作る → `fn(collection)` → 確実に close。Lambda の 1 回の呼び出しで使い切る |
|
|
9
|
+
| `lazyCollection(name, options?)` | 最初の使用時に 1 回だけ接続し、コンテナ内で再利用。失敗はキャッシュしない。`.reset()` で破棄 |
|
|
10
|
+
| `createAdminDirectory(options?)` | `@exabugs/dynamodb-client/notify`。`admins` の全件から通知の宛先(メール)を返す |
|
|
11
|
+
|
|
12
|
+
`options`: `url`(省略時は環境変数 `RECORDS_IAM_URL`)、`region`(省略時は `AWS_REGION`、無ければ `REGION`)、`timeout`(ms。Records の Lambda の timeout より短くする)。
|
|
13
|
+
|
|
14
|
+
`createAdminDirectory` の主なオプション: `recipientFields`(宛先を取るフィールド。既定 `['id']`、`['email','id']` で `email ?? id`)、`maxAdmins`(既定 200。件数が超えたら例外、ちょうどは成功)、`sortById`(id 昇順で読む)、`source`(`authorizedCollection` の `list` と同じ形。権限チェック層を通すアプリが渡す。指定すると自前で接続しない)。
|
|
15
|
+
|
|
16
|
+
## Lambda での使い方
|
|
17
|
+
|
|
18
|
+
コンテナで使い回す(コールドスタートで 1 回だけ接続):
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { lazyCollection } from '@exabugs/dynamodb-client/client/iam';
|
|
22
|
+
|
|
23
|
+
const venues = lazyCollection<VenueRecord>('venues', { timeout: 15_000 });
|
|
24
|
+
|
|
25
|
+
export const handler = async () => {
|
|
26
|
+
const active = await (await venues()).find({ status: 'active' }).toArray();
|
|
27
|
+
// ...
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
呼び出しごとに使い切る:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { withCollection } from '@exabugs/dynamodb-client/client/iam';
|
|
35
|
+
|
|
36
|
+
await withCollection<WeatherRecord>('weather', (weather) =>
|
|
37
|
+
weather.updateOne({ id }, { $set: data }, { upsert: true })
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
asanowa の `functions/weather` の `getDynamoClient`(`new DynamoClient` + `connect()` + `db()` のキャッシュ)は、`lazyCollection` の 1 行になる。
|
|
42
|
+
|
|
43
|
+
## テスト
|
|
44
|
+
|
|
45
|
+
`DynamoClient` を丸ごとモックすると `connect()` の呼び忘れを見逃す。本物の `DynamoClient` を使い、`fetch` だけ差し替える(`vi.stubGlobal('fetch', ...)`。例: `__tests__/client/recordsClient.test.ts`)。コレクションを差し替えたいときは、`lazyCollection` の戻り値の関数をアプリ側のモジュールに閉じ込め、そのモジュールをモックする。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exabugs/dynamodb-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"LICENSE",
|
|
44
44
|
"CHANGELOG.md",
|
|
45
45
|
"docs/NOTIFY.md",
|
|
46
|
-
"docs/DYNAMODB_ACCESS.md"
|
|
46
|
+
"docs/DYNAMODB_ACCESS.md",
|
|
47
|
+
"docs/RECORDS_CLIENT.md"
|
|
47
48
|
],
|
|
48
49
|
"exports": {
|
|
49
50
|
".": {
|
|
@@ -38,8 +38,10 @@ module "media" {
|
|
|
38
38
|
environment = "dev"
|
|
39
39
|
region = "ap-northeast-1"
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
# Records の IAM 版 Function URL とエイリアスの ARN(本体の出力)。media の Lambda は
|
|
42
|
+
# DynamoDB に直接触れず、Records を呼ぶ(ADR 0005)
|
|
43
|
+
records_iam_function_url = module.records.iam_function_url
|
|
44
|
+
records_iam_alias_arn = module.records.iam_alias_arn
|
|
43
45
|
|
|
44
46
|
signing_public_key_param = "/my-project/dev/media/signing-public-key"
|
|
45
47
|
image_policy_param = "/my-project/dev/media/image-policy"
|
package/terraform/media/main.tf
CHANGED
|
@@ -104,6 +104,11 @@ resource "aws_sqs_queue" "process_handler_dlq" {
|
|
|
104
104
|
name = "${local.name_prefix}-process-dlq"
|
|
105
105
|
message_retention_seconds = 1209600 # 14日(SQS上限)
|
|
106
106
|
|
|
107
|
+
# Lambda(process-handler-dlq)のイベントソースになるキューの可視性タイムアウトは、関数の
|
|
108
|
+
# timeout 以上が必須(満たさないとマッピングの作成・更新が拒否される)。AWS の推奨は 6 倍
|
|
109
|
+
# (AWS Lambda 開発者ガイド「Amazon SQS でのイベントソースマッピング」、ADR 0005)
|
|
110
|
+
visibility_timeout_seconds = var.process_handler_dlq_timeout * 6
|
|
111
|
+
|
|
107
112
|
tags = {
|
|
108
113
|
Name = "${local.name_prefix}-process-dlq"
|
|
109
114
|
}
|
|
@@ -180,19 +185,19 @@ resource "aws_iam_role_policy" "process_handler_s3" {
|
|
|
180
185
|
})
|
|
181
186
|
}
|
|
182
187
|
|
|
183
|
-
resource "aws_iam_role_policy" "
|
|
184
|
-
name = "
|
|
188
|
+
resource "aws_iam_role_policy" "process_handler_records" {
|
|
189
|
+
name = "records-invoke"
|
|
185
190
|
role = aws_iam_role.process_handler.id
|
|
186
191
|
|
|
192
|
+
# DynamoDB には直接触れず、Records の IAM 版 Function URL を呼ぶ(ADR 0005)
|
|
187
193
|
policy = jsonencode({
|
|
188
194
|
Version = "2012-10-17"
|
|
189
195
|
Statement = [
|
|
190
196
|
{
|
|
191
197
|
Effect = "Allow"
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
Resource = [var.table_arn, "${var.table_arn}/index/*"]
|
|
198
|
+
# 2025 年 10 月以降の新しい Function URL(AWS_IAM)は両方の権限が必要(AWS 公式ドキュメント urls-auth、ADR 0005)
|
|
199
|
+
Action = ["lambda:InvokeFunction", "lambda:InvokeFunctionUrl"]
|
|
200
|
+
Resource = var.records_iam_alias_arn
|
|
196
201
|
}
|
|
197
202
|
]
|
|
198
203
|
})
|
|
@@ -265,20 +270,16 @@ resource "aws_lambda_function" "process_handler" {
|
|
|
265
270
|
|
|
266
271
|
environment {
|
|
267
272
|
variables = {
|
|
268
|
-
REGION
|
|
269
|
-
|
|
270
|
-
IMAGE_POLICY_PARAM
|
|
271
|
-
SHADOW_CREATED_AT_FIELD = var.shadow_created_at_field
|
|
272
|
-
SHADOW_UPDATED_AT_FIELD = var.shadow_updated_at_field
|
|
273
|
-
SHADOW_STRING_MAX_BYTES = var.shadow_string_max_bytes
|
|
274
|
-
SHADOW_NUMBER_PADDING = var.shadow_number_padding
|
|
273
|
+
REGION = var.region
|
|
274
|
+
RECORDS_IAM_URL = var.records_iam_function_url
|
|
275
|
+
IMAGE_POLICY_PARAM = var.image_policy_param
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
depends_on = [
|
|
279
280
|
aws_cloudwatch_log_group.process_handler,
|
|
280
281
|
aws_iam_role_policy.process_handler_s3,
|
|
281
|
-
aws_iam_role_policy.
|
|
282
|
+
aws_iam_role_policy.process_handler_records,
|
|
282
283
|
aws_iam_role_policy.process_handler_ssm,
|
|
283
284
|
]
|
|
284
285
|
|
|
@@ -388,17 +389,19 @@ resource "aws_iam_role_policy" "process_handler_dlq_s3" {
|
|
|
388
389
|
})
|
|
389
390
|
}
|
|
390
391
|
|
|
391
|
-
resource "aws_iam_role_policy" "
|
|
392
|
-
name = "
|
|
392
|
+
resource "aws_iam_role_policy" "process_handler_dlq_records" {
|
|
393
|
+
name = "records-invoke"
|
|
393
394
|
role = aws_iam_role.process_handler_dlq.id
|
|
394
395
|
|
|
396
|
+
# DynamoDB には直接触れず、Records の IAM 版 Function URL を呼ぶ(ADR 0005)
|
|
395
397
|
policy = jsonencode({
|
|
396
398
|
Version = "2012-10-17"
|
|
397
399
|
Statement = [
|
|
398
400
|
{
|
|
399
|
-
Effect
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
Effect = "Allow"
|
|
402
|
+
# 2025 年 10 月以降の新しい Function URL(AWS_IAM)は両方の権限が必要(AWS 公式ドキュメント urls-auth、ADR 0005)
|
|
403
|
+
Action = ["lambda:InvokeFunction", "lambda:InvokeFunctionUrl"]
|
|
404
|
+
Resource = var.records_iam_alias_arn
|
|
402
405
|
}
|
|
403
406
|
]
|
|
404
407
|
})
|
|
@@ -431,24 +434,20 @@ resource "aws_lambda_function" "process_handler_dlq" {
|
|
|
431
434
|
architectures = ["arm64"]
|
|
432
435
|
handler = "media-process-handler-dlq.handler"
|
|
433
436
|
|
|
434
|
-
timeout =
|
|
437
|
+
timeout = var.process_handler_dlq_timeout
|
|
435
438
|
memory_size = 256
|
|
436
439
|
|
|
437
440
|
environment {
|
|
438
441
|
variables = {
|
|
439
|
-
REGION
|
|
440
|
-
|
|
441
|
-
SHADOW_CREATED_AT_FIELD = var.shadow_created_at_field
|
|
442
|
-
SHADOW_UPDATED_AT_FIELD = var.shadow_updated_at_field
|
|
443
|
-
SHADOW_STRING_MAX_BYTES = var.shadow_string_max_bytes
|
|
444
|
-
SHADOW_NUMBER_PADDING = var.shadow_number_padding
|
|
442
|
+
REGION = var.region
|
|
443
|
+
RECORDS_IAM_URL = var.records_iam_function_url
|
|
445
444
|
}
|
|
446
445
|
}
|
|
447
446
|
|
|
448
447
|
depends_on = [
|
|
449
448
|
aws_cloudwatch_log_group.process_handler_dlq,
|
|
450
449
|
aws_iam_role_policy.process_handler_dlq_sqs,
|
|
451
|
-
aws_iam_role_policy.
|
|
450
|
+
aws_iam_role_policy.process_handler_dlq_records,
|
|
452
451
|
aws_iam_role_policy.process_handler_dlq_s3,
|
|
453
452
|
]
|
|
454
453
|
|
|
@@ -15,14 +15,25 @@ variable "region" {
|
|
|
15
15
|
type = string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
variable "
|
|
19
|
-
description = "
|
|
18
|
+
variable "records_iam_function_url" {
|
|
19
|
+
description = "Records の IAM 認証版の Function URL(本体の出力 iam_function_url。認証なし版の URL ではない)。media の Lambda はこれを SigV4 署名で呼び、DynamoDB には直接触れない(ADR 0005)"
|
|
20
20
|
type = string
|
|
21
|
+
|
|
22
|
+
# IAM 版と認証なし版は URL では区別できないので、形だけ検査する
|
|
23
|
+
validation {
|
|
24
|
+
condition = can(regex("^https://[a-z0-9]+\\.lambda-url\\.[a-z0-9-]+\\.on\\.aws/$", var.records_iam_function_url))
|
|
25
|
+
error_message = "Records の IAM 認証版の Function URL(dynamodb_client_api_iam_function_url)を渡すこと。形式: https://<id>.lambda-url.<region>.on.aws/(末尾は /)。"
|
|
26
|
+
}
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
variable "
|
|
24
|
-
description = "
|
|
29
|
+
variable "records_iam_alias_arn" {
|
|
30
|
+
description = "Records の IAM 版エイリアスの ARN(本体の出力 iam_alias_arn)。lambda:InvokeFunctionUrl と lambda:InvokeFunction の許可の対象(ADR 0005)"
|
|
25
31
|
type = string
|
|
32
|
+
|
|
33
|
+
validation {
|
|
34
|
+
condition = can(regex("^arn:aws[a-z-]*:lambda:[a-z0-9-]+:[0-9]{12}:function:[^:]+:[^:]+$", var.records_iam_alias_arn))
|
|
35
|
+
error_message = "Records の IAM 版エイリアスの ARN(dynamodb_client_api_iam_alias_arn。…:function:<名前>:<エイリアス> の修飾つき)を渡すこと。"
|
|
36
|
+
}
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
variable "image_cache_ttl_days" {
|
|
@@ -37,33 +48,6 @@ variable "allowed_upload_origins" {
|
|
|
37
48
|
default = []
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
# 呼び出し側の既存テーブルに相乗りするため、process-handlerが生成するShadow Recordsの
|
|
41
|
-
# 形式は呼び出し側の既存Lambda(records等)の設定と一致させる必要がある
|
|
42
|
-
# (詳細: docs/media-design.md「環境変数の一致」)。既定値は本体(terraform/variables.tf)と揃える。
|
|
43
|
-
variable "shadow_created_at_field" {
|
|
44
|
-
description = "作成日時フィールド名(呼び出し側の既存設定と一致させること)"
|
|
45
|
-
type = string
|
|
46
|
-
default = "createdAt"
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
variable "shadow_updated_at_field" {
|
|
50
|
-
description = "更新日時フィールド名(呼び出し側の既存設定と一致させること)"
|
|
51
|
-
type = string
|
|
52
|
-
default = "updatedAt"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
variable "shadow_string_max_bytes" {
|
|
56
|
-
description = "プリミティブ型の最大バイト数(呼び出し側の既存設定と一致させること)"
|
|
57
|
-
type = number
|
|
58
|
-
default = 100
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
variable "shadow_number_padding" {
|
|
62
|
-
description = "数値のパディング桁数(呼び出し側の既存設定と一致させること)"
|
|
63
|
-
type = number
|
|
64
|
-
default = 15
|
|
65
|
-
}
|
|
66
|
-
|
|
67
51
|
variable "signing_public_key_param" {
|
|
68
52
|
description = "CloudFront署名用の公開鍵(PEM形式)を保持するSSM Parameter名。値そのものは呼び出し側が別途登録する"
|
|
69
53
|
type = string
|
|
@@ -115,3 +99,9 @@ variable "route53_zone_id" {
|
|
|
115
99
|
type = string
|
|
116
100
|
default = null
|
|
117
101
|
}
|
|
102
|
+
|
|
103
|
+
variable "process_handler_dlq_timeout" {
|
|
104
|
+
description = "process-handler-dlq Lambdaのタイムアウト(秒)。Records の読み取り・書き込みのクライアント timeout(各 15 秒)と HeadObject が収まる値にする"
|
|
105
|
+
type = number
|
|
106
|
+
default = 60
|
|
107
|
+
}
|