@aligent/aws-wrappers 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/index.cjs +1911 -0
- package/cjs/index.d.ts +1 -0
- package/cjs/package.json +51 -0
- package/{src → cjs/src}/dynamodb/dynamodb.d.ts +2 -2
- package/cjs/src/index.d.ts +7 -0
- package/{src → cjs/src}/s3/s3.d.ts +2 -2
- package/{src → cjs/src}/secrets-manager/secrets-manager.d.ts +2 -2
- package/{src → cjs/src}/sfn/sfn.d.ts +2 -2
- package/{src → cjs/src}/sns/sns.d.ts +2 -2
- package/{src → cjs/src}/sqs/sqs.d.ts +2 -2
- package/{src → cjs/src}/ssm/ssm.d.ts +2 -2
- package/{src → cjs/src}/util/redact.d.ts +2 -2
- package/esm/index.d.ts +1 -0
- package/esm/index.mjs +1903 -0
- package/esm/package.json +51 -0
- package/esm/src/dynamodb/dynamodb.d.ts +127 -0
- package/esm/src/index.d.ts +7 -0
- package/esm/src/s3/s3.d.ts +131 -0
- package/esm/src/secrets-manager/secrets-manager.d.ts +78 -0
- package/esm/src/sfn/sfn.d.ts +38 -0
- package/esm/src/sns/sns.d.ts +48 -0
- package/esm/src/sqs/sqs.d.ts +60 -0
- package/esm/src/ssm/ssm.d.ts +84 -0
- package/{src/util/redact.js → esm/src/util/redact.d.ts} +2 -13
- package/esm/src/util/truncate.d.ts +15 -0
- package/package.json +25 -7
- package/CLAUDE.md +0 -172
- package/src/dynamodb/dynamodb.js +0 -308
- package/src/index.d.ts +0 -7
- package/src/index.js +0 -17
- package/src/s3/s3.js +0 -244
- package/src/secrets-manager/secrets-manager.js +0 -152
- package/src/sfn/sfn.js +0 -74
- package/src/sns/sns.js +0 -110
- package/src/sqs/sqs.js +0 -134
- package/src/ssm/ssm.js +0 -144
- package/src/util/truncate.js +0 -36
- package/tsconfig.lib.tsbuildinfo +0 -1
- /package/{src → cjs/src}/util/truncate.d.ts +0 -0
package/package.json
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aligent/aws-wrappers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Opinionated AWS SDK wrappers with Powertools logging and X-Ray tracing",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
"main": "./cjs/index.cjs",
|
|
6
|
+
"module": "./esm/index.mjs",
|
|
7
|
+
"types": "./cjs/src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./esm/src/index.d.ts",
|
|
12
|
+
"default": "./esm/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./cjs/src/index.d.ts",
|
|
16
|
+
"default": "./cjs/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"cjs",
|
|
23
|
+
"esm",
|
|
24
|
+
"README.md",
|
|
25
|
+
"docs"
|
|
26
|
+
],
|
|
8
27
|
"repository": {
|
|
9
28
|
"type": "git",
|
|
10
29
|
"url": "https://github.com/aligent/microservice-development-utilities.git",
|
|
@@ -27,6 +46,5 @@
|
|
|
27
46
|
"license": "MIT",
|
|
28
47
|
"devDependencies": {
|
|
29
48
|
"aws-sdk-client-mock": "^4.1.0"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/CLAUDE.md
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md — @aligent/aws-wrappers
|
|
2
|
-
|
|
3
|
-
Guidance for Claude Code when working in this package. Read alongside the repo-root `CLAUDE.md`.
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
Each `*Service` class wraps a single AWS SDK client. The wrapper bundles:
|
|
8
|
-
|
|
9
|
-
- a Powertools `Logger` (one `logger.info` line at the start of every public method),
|
|
10
|
-
- X-Ray tracing via `captureAWSv3Client`,
|
|
11
|
-
- ergonomic helpers that smooth over recurring SDK quirks (auto-pagination, auto-chunking, JSON helpers, retry-on-`UnprocessedItems`, etc.).
|
|
12
|
-
|
|
13
|
-
Callers who need raw SDK access drop down to the SDK client directly — the wrappers do not try to be a full SDK replacement.
|
|
14
|
-
|
|
15
|
-
## Layout
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
packages/aws-wrappers/src/
|
|
19
|
-
├── <service>/
|
|
20
|
-
│ ├── <service>.ts # the *Service class
|
|
21
|
-
│ └── <service>.test.ts # co-located tests
|
|
22
|
-
└── index.ts # named exports of every *Service class
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
One folder per service, lowercase. No barrel files inside service folders — `index.ts` imports the class directly from `<service>/<service>`.
|
|
26
|
-
|
|
27
|
-
## Locked-in conventions
|
|
28
|
-
|
|
29
|
-
These are non-negotiable across every service in the package — change them only with explicit user sign-off.
|
|
30
|
-
|
|
31
|
-
### Constructor
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
constructor(opts?: { logger?: Logger; client?: <ServiceClient> })
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
- `logger` defaults to `new Logger()`, which picks up `POWERTOOLS_SERVICE_NAME` from the environment. Do **not** pass `serviceName` in the default — env-driven service naming is the Powertools convention.
|
|
38
|
-
- `client` defaults to `captureAWSv3Client(new <ServiceClient>())`. When the caller supplies a client, the wrapper does **not** apply X-Ray instrumentation — that's the caller's call.
|
|
39
|
-
- No `clientConfig` / `region` / `endpoint` options. Callers needing those construct their own client and pass it via `client`.
|
|
40
|
-
|
|
41
|
-
### Logging
|
|
42
|
-
|
|
43
|
-
Every public method emits exactly one `logger.info('<verb> <noun>', { input })` line at the start. The shape of `input` is **level-driven**:
|
|
44
|
-
|
|
45
|
-
- At `DEBUG` (e.g. `POWERTOOLS_LOG_LEVEL=DEBUG`), the full SDK input is logged. Operators have explicitly opted into seeing payloads, secret material, and PII.
|
|
46
|
-
- At any other level, only a per-method **safe-fields allowlist** is logged.
|
|
47
|
-
|
|
48
|
-
The mechanism is `filterFieldsForLogLevel(logger, input, SAFE_FIELDS)` in `src/util/redact.ts`. Internal-only helper, not exported from `src/index.ts`.
|
|
49
|
-
|
|
50
|
-
#### Conventions for the allowlist
|
|
51
|
-
|
|
52
|
-
- **Module-level constant per method**, typed as `ReadonlyArray<keyof CommandInput>`, named `<METHOD>_SAFE_FIELDS`. TSDoc explains what's omitted and why.
|
|
53
|
-
- **Targeted application** — only methods where there's an actual omission use the helper. Methods whose input is already a tight `Required<Pick<...>>` or contains no sensitive fields (e.g. `S3.getObject`, `SFN.listExecutions`) keep their current `{ input }` shape.
|
|
54
|
-
- **Maximal safe set** — include every field *except* known payload, secret, or PII carriers. The level-based design provides the safety valve; the INFO log should still be operationally useful.
|
|
55
|
-
- **Batch methods stay bespoke** — methods that already compute a derived field (e.g. `entryCount`, `keyCount`, `tables`) inline the DEBUG check directly rather than using `filterFieldsForLogLevel`, since the helper only picks input keys and can't synthesise computed fields. A comment explains why.
|
|
56
|
-
|
|
57
|
-
#### Currently redacted
|
|
58
|
-
|
|
59
|
-
- **S3** — `putObject` / `putJsonObject` omit `Body`. Batch methods (`deleteObjects`, `emptyBucket`) log `{ bucket, keyCount }`.
|
|
60
|
-
- **SNS** — `publish` omits `Message`, `Subject`, `MessageAttributes`, `PhoneNumber`. `publishBatch` logs `{ TopicArn, entryCount }`.
|
|
61
|
-
- **SQS** — `sendMessage` omits `MessageBody`, `MessageAttributes`. Batch methods log `{ QueueUrl, entryCount }`.
|
|
62
|
-
- **DynamoDB** — `getItem` / `deleteItem` omit `Key`. `putItem` omits `Item`. `updateItem` omits `Key` and `ExpressionAttributeValues`. `query` / `scan` / `paginateItems` / `paginateScan` omit `ExpressionAttributeValues`. `batchGet` / `batchWrite` log `{ tables: Object.keys(RequestItems) }` only.
|
|
63
|
-
- **SecretsManager** — write methods omit `SecretString` / `SecretBinary`.
|
|
64
|
-
- **SSM** — `putParameter` omits `Value`.
|
|
65
|
-
- **SFN** — `startExecution` omits the execution `input` (often carries PII).
|
|
66
|
-
|
|
67
|
-
Adding a new redacted method: define a `<METHOD>_SAFE_FIELDS` constant near the top of the service file with TSDoc, wire `filterFieldsForLogLevel(this.logger, input, FIELDS)` into the `logger.info` call, and add a single `expect(loggedInput).not.toHaveProperty('<sensitive>')` test against an INFO-level logger to lock in the security property.
|
|
68
|
-
|
|
69
|
-
### Patterns
|
|
70
|
-
|
|
71
|
-
- **Auto-pagination, flat array**: `S3.listObjects` / `getAllObjects` / `emptyBucket`, `SSM.getParametersByPath`, `SFN.listExecutions`. Used when the result set is bounded in practice.
|
|
72
|
-
- **Generator pagination, yield items**: `DynamoDB.paginateItems` / `paginateScan`. Used when the result set is potentially unbounded — peak memory stays bounded by one page.
|
|
73
|
-
- **Auto-chunking**: `S3.deleteObjects` / `emptyBucket` (1000), `SQS.sendMessageBatch` / `deleteMessageBatch` (10), `SNS.publishBatch` (10). Mirrors the SDK-enforced per-request cap so callers don't have to.
|
|
74
|
-
|
|
75
|
-
### Opt-in payload truncation
|
|
76
|
-
|
|
77
|
-
`SNS.publish` and `SQS.sendMessage` expose an opt-in truncation knob for callers who prefer data loss over SDK-level failure on oversize (e.g. notification flows where dropped detail beats a thrown error). The default is **off** — fail-fast at the SDK is the right behaviour for most code paths.
|
|
78
|
-
|
|
79
|
-
- Constructor option `truncate?: boolean` sets the per-instance default.
|
|
80
|
-
- Per-call option `{ truncate?: boolean }` overrides the instance default.
|
|
81
|
-
- When enabled, the wrapper uses `truncateUtf8` for byte-bounded fields (256 KB for `Message` / `MessageBody`) and `truncateCodepoints` for char-bounded fields (100 chars for SNS `Subject`). Both helpers live in `src/util/truncate.ts` and respect codepoint boundaries (no half-emoji, no malformed UTF-8).
|
|
82
|
-
- Each truncating call emits a single `logger.warn('Truncated <service> <op> input', { fields: [...] })` line listing what was modified.
|
|
83
|
-
|
|
84
|
-
If you find yourself adding truncation to a third method, raise it with the user first — opt-out flags are the more conservative default.
|
|
85
|
-
|
|
86
|
-
### DynamoDB specifics
|
|
87
|
-
|
|
88
|
-
- Backed by `DynamoDBDocumentClient`. Always wrap the base `DynamoDBClient` with `captureAWSv3Client` **before** passing it to `DynamoDBDocumentClient.from(...)`, so X-Ray captures every command.
|
|
89
|
-
- `marshallOptions: { removeUndefinedValues: true }`.
|
|
90
|
-
- All commands and paginators come from `@aws-sdk/lib-dynamodb`. Never import marshaling helpers from `@aws-sdk/util-dynamodb` — that's what the doc client is for.
|
|
91
|
-
- Generic typing convention:
|
|
92
|
-
- Methods that return a single item or yield items (`paginateItems`, `paginateScan`): `AsyncGenerator<T>`.
|
|
93
|
-
- Methods that return the full command output (`query`, `scan`): preserve the output and generically type only the data-bearing field (`Items?: T[]`).
|
|
94
|
-
- Key-bearing methods (`getItem`, `updateItem`, `deleteItem`) take two generics — `K extends Record<string, unknown>` for the partition / sort key shape, `R extends Record<string, unknown>` for the return type. The input is threaded through `WithTypedKey<TInput, K>` so the SDK input is preserved with the typed `Key` substituted.
|
|
95
|
-
- `batchGet` is intentionally **not** generic — multi-table `Responses` can't be soundly described by a single `T`. Document this in TSDoc whenever the method is touched.
|
|
96
|
-
- All generics default to `Record<string, unknown>` so callers can omit them.
|
|
97
|
-
|
|
98
|
-
## Adding a new service
|
|
99
|
-
|
|
100
|
-
Walk through these in order. Each step is small; nothing is automatable enough to be worth a generator.
|
|
101
|
-
|
|
102
|
-
1. **Confirm scope with the user.** Run the questions in the next section before writing code. Don't infer the answers from the SDK.
|
|
103
|
-
2. **Install the SDK client** as a runtime dependency of the package:
|
|
104
|
-
```sh
|
|
105
|
-
npm install --workspace=@aligent/aws-wrappers @aws-sdk/client-<service>
|
|
106
|
-
```
|
|
107
|
-
Never hand-edit `package.json`. ESLint's `@nx/dependency-checks` rule will flag unused deps via `--fix` if you install too early — install per-service as you implement.
|
|
108
|
-
3. **Create the folder**: `packages/aws-wrappers/src/<service>/`, with `<service>.ts` and `<service>.test.ts`.
|
|
109
|
-
4. **Implement the class** following the locked-in conventions above. Mirror the structure of an existing service of similar shape — `SecretsManagerService` is the simplest skeleton; `DynamoDBService` is the most complex.
|
|
110
|
-
5. **Add tests with `aws-sdk-client-mock`**. Default-construction test (`expect(() => new XService()).not.toThrow()`) plus targeted coverage on non-trivial methods. For any method that uses an SDK paginator (`paginate*`), pass a real client instance via the constructor — see "Testing notes" below.
|
|
111
|
-
6. **Add a named export to `src/index.ts`** in alphabetical order.
|
|
112
|
-
7. **Run** `npx nx run aws-wrappers:lint --fix`, `:typecheck`, `:test --coverage`, `:typedoc`. The 80% workspace coverage threshold is enforced.
|
|
113
|
-
8. **Update the package `README.md`** with a worked example under a new `## <Service Name>` section.
|
|
114
|
-
9. **Commit** with the active ticket prefix.
|
|
115
|
-
10. **Ask the user whether to run the `code-reviewer` sub-agent** over the change before opening the PR. The reviewer catches drift from the locked-in conventions (logging shape, generics, pagination/chunking patterns) and the kinds of defensive-coverage gaps the workspace gate doesn't enforce. Surface its punch list to the user — don't auto-apply fixes.
|
|
116
|
-
|
|
117
|
-
## Questions to ask the user
|
|
118
|
-
|
|
119
|
-
### When adding a new service
|
|
120
|
-
|
|
121
|
-
Don't write code until each of these is answered. Defaults in **bold**.
|
|
122
|
-
|
|
123
|
-
- **Method coverage**: which SDK operations should the wrapper expose? Default: only the ones with concrete near-term callers — every method is API surface area to maintain.
|
|
124
|
-
- **Pagination**: for each `List*` / `Get*ByPath` / etc. operation:
|
|
125
|
-
- Auto-paginate and return a flat array? (**default** when result set is bounded by filters / retention)
|
|
126
|
-
- Expose as an `AsyncGenerator` that yields items? (**default** when result set is potentially unbounded)
|
|
127
|
-
- Pass-through, leave pagination to caller? (only when the caller usually wants pagination metadata)
|
|
128
|
-
- **Chunking**: any operation with a per-request entry/key cap (e.g. SNS `PublishBatch` at 10) — auto-chunk to the cap (**default**) or pass-through?
|
|
129
|
-
- **Generic typing**:
|
|
130
|
-
- For methods that return a single data-bearing item — return generic `T | undefined` (**default**).
|
|
131
|
-
- For methods that return command output with metadata — preserve the output, generic on the data-bearing field (`Items?: T[]`, `Attributes?: T`).
|
|
132
|
-
- For multi-shape responses (cf. DynamoDB `batchGet`) — **not generic**, document why in TSDoc.
|
|
133
|
-
- Default the generic to `T extends Record<string, unknown> = Record<string, unknown>` so callers can omit it.
|
|
134
|
-
- **Logging shape**: does the input contain anything sensitive (credentials, large payloads, message bodies)? If yes, follow the documented exception pattern (`{ Bucket, Key }`, `{ entryCount, ... }`). Default to `{ input }`.
|
|
135
|
-
- **Error semantics**: any operations where the wrapper should retry, swallow, or surface differently than the SDK? Capture the rationale in TSDoc. Examples already in this package: `batchWrite` (retry `UnprocessedItems`), `getSecret` (throw on missing `SecretString`).
|
|
136
|
-
- **Input shape**: pass-through SDK input (**default**) or a tight `Required<Pick<...>>` projection (the S3 pattern)? Tight shape is appropriate when the SDK has many rarely-used optional fields and exposing them noises up TS errors.
|
|
137
|
-
- **Defaults bakedin**: e.g. `SSM` bakes in `WithDecryption: true` with no opt-out. Capture every such "no opt-out" decision in the class-level TSDoc.
|
|
138
|
-
|
|
139
|
-
### When changing an existing service
|
|
140
|
-
|
|
141
|
-
- Is this change additive (new method) or modifying an existing public method?
|
|
142
|
-
- If modifying: is the package already published? If yes, this is a SemVer-major change — flag it before implementing.
|
|
143
|
-
- Does it touch any of the locked-in conventions above? If yes, surface to the user first.
|
|
144
|
-
- Will any of the existing tests need to change? If yes, that's a load-bearing signal — verify whether the existing test behaviour was load-bearing for a downstream caller.
|
|
145
|
-
|
|
146
|
-
Once the change is in and tests pass, ask the user whether to run the `code-reviewer` sub-agent over the diff before opening the PR — same rationale as step 10 of the new-service flow.
|
|
147
|
-
|
|
148
|
-
## Testing notes
|
|
149
|
-
|
|
150
|
-
- `aws-sdk-client-mock` patches the prototype `send` method of all instances of the SDK client class. The mock object itself is **not** a usable client — for paginators it fails `instanceof` checks. **For any method that uses an SDK paginator, pass a real client instance via the constructor:**
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
const mock = mockClient(SSMClient); // global intercept
|
|
154
|
-
const service = new SSMService({ client: new SSMClient({}) }); // real instance
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
The mock still intercepts every `.send` call. The bare cast `mock as unknown as SSMClient` works for non-paginator methods but is brittle — prefer the real-instance pattern uniformly.
|
|
158
|
-
|
|
159
|
-
- Error assertions: `await expect(fn()).rejects.toThrow(...)`. Do **not** use the `try / catch + // eslint-disable-next-line no-empty` pattern that exists in older `microservice-util-lib` tests.
|
|
160
|
-
|
|
161
|
-
- Coverage gate is 80% workspace-global on lines / branches / functions / statements. For thin pass-through methods, a one-shot "verify the SDK command was sent" test is enough — these are visually verifiable but still need to keep the gate green.
|
|
162
|
-
|
|
163
|
-
## Out of scope
|
|
164
|
-
|
|
165
|
-
These came up during the initial design and were explicitly deferred:
|
|
166
|
-
|
|
167
|
-
- Moving the existing AWS utilities (`S3Dao`, `fetchSsmParams`, `get-aws-id-from-arn`) out of `microservice-util-lib` into this package.
|
|
168
|
-
- Deprecating those existing utilities or adding `@deprecated` tags.
|
|
169
|
-
- Migration guide from the legacy utilities to the new wrappers.
|
|
170
|
-
- Integration tests against LocalStack or real AWS.
|
|
171
|
-
- CDK constructs in `nx-cdk` corresponding to these wrappers.
|
|
172
|
-
- Wrappers for additional AWS services (EventBridge, Kinesis, etc.) — add them as separate tickets following the "Adding a new service" flow.
|
package/src/dynamodb/dynamodb.js
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DynamoDBService = void 0;
|
|
4
|
-
const logger_1 = require("@aws-lambda-powertools/logger");
|
|
5
|
-
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
6
|
-
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
7
|
-
const aws_xray_sdk_core_1 = require("aws-xray-sdk-core");
|
|
8
|
-
const redact_1 = require("../util/redact");
|
|
9
|
-
const BATCH_WRITE_MAX_ATTEMPTS = 5;
|
|
10
|
-
const BATCH_WRITE_BASE_DELAY_MS = 200;
|
|
11
|
-
/**
|
|
12
|
-
* Fields safe to log at INFO. Omits `Key` (may carry customer IDs / tenant IDs).
|
|
13
|
-
* `POWERTOOLS_LOG_LEVEL=DEBUG` unlocks the full input.
|
|
14
|
-
*/
|
|
15
|
-
const GET_ITEM_SAFE_FIELDS = [
|
|
16
|
-
'TableName',
|
|
17
|
-
'ConsistentRead',
|
|
18
|
-
'ProjectionExpression',
|
|
19
|
-
'ReturnConsumedCapacity',
|
|
20
|
-
'ExpressionAttributeNames',
|
|
21
|
-
];
|
|
22
|
-
/**
|
|
23
|
-
* Fields safe to log at INFO. Omits `Item` (the payload itself) and
|
|
24
|
-
* `ExpressionAttributeValues` (values bound to ConditionExpression, often PII).
|
|
25
|
-
* `POWERTOOLS_LOG_LEVEL=DEBUG` unlocks the full input.
|
|
26
|
-
*/
|
|
27
|
-
const PUT_ITEM_SAFE_FIELDS = [
|
|
28
|
-
'TableName',
|
|
29
|
-
'ConditionExpression',
|
|
30
|
-
'ExpressionAttributeNames',
|
|
31
|
-
'ReturnValues',
|
|
32
|
-
'ReturnConsumedCapacity',
|
|
33
|
-
'ReturnItemCollectionMetrics',
|
|
34
|
-
'ReturnValuesOnConditionCheckFailure',
|
|
35
|
-
];
|
|
36
|
-
/**
|
|
37
|
-
* Fields safe to log at INFO. Omits `Key` and `ExpressionAttributeValues`.
|
|
38
|
-
* `POWERTOOLS_LOG_LEVEL=DEBUG` unlocks the full input.
|
|
39
|
-
*/
|
|
40
|
-
const UPDATE_ITEM_SAFE_FIELDS = [
|
|
41
|
-
'TableName',
|
|
42
|
-
'UpdateExpression',
|
|
43
|
-
'ConditionExpression',
|
|
44
|
-
'ExpressionAttributeNames',
|
|
45
|
-
'ReturnValues',
|
|
46
|
-
'ReturnConsumedCapacity',
|
|
47
|
-
'ReturnItemCollectionMetrics',
|
|
48
|
-
'ReturnValuesOnConditionCheckFailure',
|
|
49
|
-
];
|
|
50
|
-
/**
|
|
51
|
-
* Fields safe to log at INFO. Omits `Key` and `ExpressionAttributeValues`
|
|
52
|
-
* (the latter binds to ConditionExpression and may carry PII).
|
|
53
|
-
* `POWERTOOLS_LOG_LEVEL=DEBUG` unlocks the full input.
|
|
54
|
-
*/
|
|
55
|
-
const DELETE_ITEM_SAFE_FIELDS = [
|
|
56
|
-
'TableName',
|
|
57
|
-
'ConditionExpression',
|
|
58
|
-
'ExpressionAttributeNames',
|
|
59
|
-
'ReturnValues',
|
|
60
|
-
'ReturnConsumedCapacity',
|
|
61
|
-
'ReturnItemCollectionMetrics',
|
|
62
|
-
'ReturnValuesOnConditionCheckFailure',
|
|
63
|
-
];
|
|
64
|
-
/**
|
|
65
|
-
* Fields safe to log at INFO for `query` and `paginateItems`. Omits
|
|
66
|
-
* `ExpressionAttributeValues` (values often carry PII) and `ExclusiveStartKey`
|
|
67
|
-
* (pagination cursor includes Key shape). `POWERTOOLS_LOG_LEVEL=DEBUG` unlocks
|
|
68
|
-
* the full input.
|
|
69
|
-
*/
|
|
70
|
-
const QUERY_SAFE_FIELDS = [
|
|
71
|
-
'TableName',
|
|
72
|
-
'IndexName',
|
|
73
|
-
'KeyConditionExpression',
|
|
74
|
-
'FilterExpression',
|
|
75
|
-
'ProjectionExpression',
|
|
76
|
-
'ExpressionAttributeNames',
|
|
77
|
-
'ConsistentRead',
|
|
78
|
-
'ScanIndexForward',
|
|
79
|
-
'Select',
|
|
80
|
-
'Limit',
|
|
81
|
-
'ReturnConsumedCapacity',
|
|
82
|
-
];
|
|
83
|
-
/**
|
|
84
|
-
* Fields safe to log at INFO for `scan` and `paginateScan`. Omits
|
|
85
|
-
* `ExpressionAttributeValues` and `ExclusiveStartKey`.
|
|
86
|
-
* `POWERTOOLS_LOG_LEVEL=DEBUG` unlocks the full input.
|
|
87
|
-
*/
|
|
88
|
-
const SCAN_SAFE_FIELDS = [
|
|
89
|
-
'TableName',
|
|
90
|
-
'IndexName',
|
|
91
|
-
'FilterExpression',
|
|
92
|
-
'ProjectionExpression',
|
|
93
|
-
'ExpressionAttributeNames',
|
|
94
|
-
'ConsistentRead',
|
|
95
|
-
'Select',
|
|
96
|
-
'Limit',
|
|
97
|
-
'Segment',
|
|
98
|
-
'TotalSegments',
|
|
99
|
-
'ReturnConsumedCapacity',
|
|
100
|
-
];
|
|
101
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
102
|
-
const backoffDelay = (attempt) => {
|
|
103
|
-
const exp = BATCH_WRITE_BASE_DELAY_MS * 2 ** attempt;
|
|
104
|
-
return exp + Math.random() * exp;
|
|
105
|
-
};
|
|
106
|
-
/**
|
|
107
|
-
* Wrapper around the AWS DynamoDB Document client providing structured
|
|
108
|
-
* Powertools logging and X-Ray tracing by default.
|
|
109
|
-
*
|
|
110
|
-
* Items are automatically marshalled / unmarshalled via the document client —
|
|
111
|
-
* callers work with plain TypeScript objects in both directions.
|
|
112
|
-
*/
|
|
113
|
-
class DynamoDBService {
|
|
114
|
-
client;
|
|
115
|
-
logger;
|
|
116
|
-
/**
|
|
117
|
-
* @param opts.logger - Optional Powertools logger. Defaults to `new Logger()`,
|
|
118
|
-
* which picks up `POWERTOOLS_SERVICE_NAME` from the environment.
|
|
119
|
-
* @param opts.client - Optional pre-configured `DynamoDBDocumentClient`.
|
|
120
|
-
* When supplied, the wrapper does not apply X-Ray instrumentation. When
|
|
121
|
-
* omitted, a default `DynamoDBClient` is wrapped with `captureAWSv3Client`
|
|
122
|
-
* *before* being passed to `DynamoDBDocumentClient.from`, so X-Ray
|
|
123
|
-
* tracing captures every DynamoDB call.
|
|
124
|
-
*/
|
|
125
|
-
constructor(opts) {
|
|
126
|
-
this.client =
|
|
127
|
-
opts?.client ??
|
|
128
|
-
lib_dynamodb_1.DynamoDBDocumentClient.from((0, aws_xray_sdk_core_1.captureAWSv3Client)(new client_dynamodb_1.DynamoDBClient({})), {
|
|
129
|
-
marshallOptions: { removeUndefinedValues: true },
|
|
130
|
-
});
|
|
131
|
-
this.logger = opts?.logger ?? new logger_1.Logger();
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Get an item from DynamoDB.
|
|
135
|
-
* @template K - Shape of the partition / sort key.
|
|
136
|
-
* @template R - Expected unmarshalled item shape.
|
|
137
|
-
* @returns The item, or `undefined` if not found.
|
|
138
|
-
*/
|
|
139
|
-
async getItem(input) {
|
|
140
|
-
this.logger.info('Getting DynamoDB item', {
|
|
141
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, GET_ITEM_SAFE_FIELDS),
|
|
142
|
-
});
|
|
143
|
-
const response = await this.client.send(new lib_dynamodb_1.GetCommand(input));
|
|
144
|
-
return response.Item;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Put an item into DynamoDB. The caller's `Item` is typed as `T`, which
|
|
148
|
-
* the document client marshalls automatically.
|
|
149
|
-
* @template T - Type of the item being stored.
|
|
150
|
-
*/
|
|
151
|
-
async putItem(input) {
|
|
152
|
-
this.logger.info('Putting DynamoDB item', {
|
|
153
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, PUT_ITEM_SAFE_FIELDS),
|
|
154
|
-
});
|
|
155
|
-
return this.client.send(new lib_dynamodb_1.PutCommand(input));
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Update an item in DynamoDB. The `Attributes` field on the response is
|
|
159
|
-
* typed as `R` — the caller should choose `R` to match their
|
|
160
|
-
* `ReturnValues` setting:
|
|
161
|
-
* - `NONE` (default): no `Attributes` returned.
|
|
162
|
-
* - `ALL_OLD` / `ALL_NEW`: full item.
|
|
163
|
-
* - `UPDATED_OLD` / `UPDATED_NEW`: only updated attributes (partial).
|
|
164
|
-
* @template K - Shape of the partition / sort key.
|
|
165
|
-
* @template R - Expected shape of the returned `Attributes`.
|
|
166
|
-
*/
|
|
167
|
-
async updateItem(input) {
|
|
168
|
-
this.logger.info('Updating DynamoDB item', {
|
|
169
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, UPDATE_ITEM_SAFE_FIELDS),
|
|
170
|
-
});
|
|
171
|
-
const response = await this.client.send(new lib_dynamodb_1.UpdateCommand(input));
|
|
172
|
-
return response;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Delete an item from DynamoDB. The `Attributes` field on the response is
|
|
176
|
-
* typed as `R` — relevant when `ReturnValues: 'ALL_OLD'` is set.
|
|
177
|
-
* @template K - Shape of the partition / sort key.
|
|
178
|
-
* @template R - Expected shape of the returned `Attributes`.
|
|
179
|
-
*/
|
|
180
|
-
async deleteItem(input) {
|
|
181
|
-
this.logger.info('Deleting DynamoDB item', {
|
|
182
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, DELETE_ITEM_SAFE_FIELDS),
|
|
183
|
-
});
|
|
184
|
-
const response = await this.client.send(new lib_dynamodb_1.DeleteCommand(input));
|
|
185
|
-
return response;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Execute a DynamoDB Query. The full `QueryCommandOutput` is returned with
|
|
189
|
-
* `Items` typed as `T[]` so callers retain pagination metadata
|
|
190
|
-
* (`LastEvaluatedKey`, `Count`, etc.).
|
|
191
|
-
* @template T - Expected shape of each unmarshalled item.
|
|
192
|
-
*/
|
|
193
|
-
async query(input) {
|
|
194
|
-
this.logger.info('Querying DynamoDB', {
|
|
195
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, QUERY_SAFE_FIELDS),
|
|
196
|
-
});
|
|
197
|
-
const response = await this.client.send(new lib_dynamodb_1.QueryCommand(input));
|
|
198
|
-
return response;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Scan a DynamoDB table. The full `ScanCommandOutput` is returned with
|
|
202
|
-
* `Items` typed as `T[]` so callers retain pagination metadata.
|
|
203
|
-
*
|
|
204
|
-
* Scan reads every item in the table, so cost and latency grow linearly
|
|
205
|
-
* with table size; it is rarely the right tool in a runtime service.
|
|
206
|
-
* Prefer, in order:
|
|
207
|
-
*
|
|
208
|
-
* 1. `query` with the table's partition key.
|
|
209
|
-
* 2. `query` against a GSI or LSI whose key matches your access pattern.
|
|
210
|
-
* 3. A sparse GSI populated only for the items you need to enumerate.
|
|
211
|
-
* 4. A denormalised lookup item or table maintained on write.
|
|
212
|
-
*
|
|
213
|
-
* Legitimate scan use cases are mostly one-off admin work (export,
|
|
214
|
-
* migration, audit). For those, prefer the AWS CLI or Console rather than
|
|
215
|
-
* embedding a scan in a Lambda.
|
|
216
|
-
*
|
|
217
|
-
* @template T - Expected shape of each unmarshalled item.
|
|
218
|
-
*/
|
|
219
|
-
async scan(input) {
|
|
220
|
-
this.logger.info('Scanning DynamoDB', {
|
|
221
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, SCAN_SAFE_FIELDS),
|
|
222
|
-
});
|
|
223
|
-
const response = await this.client.send(new lib_dynamodb_1.ScanCommand(input));
|
|
224
|
-
return response;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Batch-get items from one or more DynamoDB tables.
|
|
228
|
-
*
|
|
229
|
-
* Note: this method is intentionally **not** generic. `BatchGet`'s
|
|
230
|
-
* `Responses` field is a multi-table `Record<string, item[]>` whose item
|
|
231
|
-
* shapes can differ per table — no single `T` can soundly describe it.
|
|
232
|
-
* Callers should narrow the result type at the call site.
|
|
233
|
-
*/
|
|
234
|
-
async batchGet(input) {
|
|
235
|
-
// Inline DEBUG check rather than `filterFieldsForLogLevel` because
|
|
236
|
-
// `RequestItems` is a `Record<tableName, KeysAndAttributes>` — the
|
|
237
|
-
// payload (`Keys[]`) lives inside the value, not as a top-level key
|
|
238
|
-
// the helper could pick or drop.
|
|
239
|
-
const isDebug = this.logger.getLevelName() === 'DEBUG';
|
|
240
|
-
this.logger.info('Batch getting DynamoDB items', {
|
|
241
|
-
input: isDebug ? input : { tables: Object.keys(input.RequestItems ?? {}) },
|
|
242
|
-
});
|
|
243
|
-
return this.client.send(new lib_dynamodb_1.BatchGetCommand(input));
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Batch-write items to DynamoDB, retrying `UnprocessedItems` with jittered
|
|
247
|
-
* exponential backoff. Up to 5 attempts (200ms base delay). Throws when
|
|
248
|
-
* items remain unprocessed after the final attempt.
|
|
249
|
-
*/
|
|
250
|
-
async batchWrite(input) {
|
|
251
|
-
// Inline DEBUG check rather than `filterFieldsForLogLevel` because
|
|
252
|
-
// `RequestItems` is a `Record<tableName, WriteRequest[]>` — the
|
|
253
|
-
// payload (`PutRequest.Item` / `DeleteRequest.Key`) lives inside the
|
|
254
|
-
// value, not as a top-level key the helper could pick or drop.
|
|
255
|
-
const isDebug = this.logger.getLevelName() === 'DEBUG';
|
|
256
|
-
this.logger.info('Batch writing DynamoDB items', {
|
|
257
|
-
input: isDebug ? input : { tables: Object.keys(input.RequestItems ?? {}) },
|
|
258
|
-
});
|
|
259
|
-
let current = input;
|
|
260
|
-
for (let attempt = 0; attempt < BATCH_WRITE_MAX_ATTEMPTS; attempt++) {
|
|
261
|
-
const response = await this.client.send(new lib_dynamodb_1.BatchWriteCommand(current));
|
|
262
|
-
const unprocessed = response.UnprocessedItems;
|
|
263
|
-
if (!unprocessed || Object.keys(unprocessed).length === 0)
|
|
264
|
-
return response;
|
|
265
|
-
this.logger.warn('Retrying unprocessed DynamoDB items', {
|
|
266
|
-
attempt: attempt + 1,
|
|
267
|
-
tables: Object.keys(unprocessed),
|
|
268
|
-
});
|
|
269
|
-
if (attempt < BATCH_WRITE_MAX_ATTEMPTS - 1)
|
|
270
|
-
await sleep(backoffDelay(attempt));
|
|
271
|
-
current = { ...input, RequestItems: unprocessed };
|
|
272
|
-
}
|
|
273
|
-
throw new Error(`batchWrite failed after ${BATCH_WRITE_MAX_ATTEMPTS} attempts`);
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Paginate over Query results, yielding one unmarshalled item at a time.
|
|
277
|
-
* @template T - Expected shape of each yielded item.
|
|
278
|
-
*/
|
|
279
|
-
async *paginateItems(input) {
|
|
280
|
-
this.logger.info('Paginating DynamoDB query', {
|
|
281
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, QUERY_SAFE_FIELDS),
|
|
282
|
-
});
|
|
283
|
-
const paginator = (0, lib_dynamodb_1.paginateQuery)({ client: this.client }, input);
|
|
284
|
-
for await (const page of paginator) {
|
|
285
|
-
if (!page.Items)
|
|
286
|
-
continue;
|
|
287
|
-
for (const item of page.Items)
|
|
288
|
-
yield item;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Paginate over Scan results, yielding one unmarshalled item at a time.
|
|
293
|
-
* @template T - Expected shape of each yielded item.
|
|
294
|
-
*/
|
|
295
|
-
async *paginateScan(input) {
|
|
296
|
-
this.logger.info('Paginating DynamoDB scan', {
|
|
297
|
-
input: (0, redact_1.filterFieldsForLogLevel)(this.logger, input, SCAN_SAFE_FIELDS),
|
|
298
|
-
});
|
|
299
|
-
const paginator = (0, lib_dynamodb_1.paginateScan)({ client: this.client }, input);
|
|
300
|
-
for await (const page of paginator) {
|
|
301
|
-
if (!page.Items)
|
|
302
|
-
continue;
|
|
303
|
-
for (const item of page.Items)
|
|
304
|
-
yield item;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
exports.DynamoDBService = DynamoDBService;
|
package/src/index.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { DynamoDBService } from './dynamodb/dynamodb';
|
|
2
|
-
export { S3Service } from './s3/s3';
|
|
3
|
-
export { SecretsManagerService } from './secrets-manager/secrets-manager';
|
|
4
|
-
export { StepFunctionsService } from './sfn/sfn';
|
|
5
|
-
export { SNSService } from './sns/sns';
|
|
6
|
-
export { SQSService } from './sqs/sqs';
|
|
7
|
-
export { SSMService } from './ssm/ssm';
|
package/src/index.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SSMService = exports.SQSService = exports.SNSService = exports.StepFunctionsService = exports.SecretsManagerService = exports.S3Service = exports.DynamoDBService = void 0;
|
|
4
|
-
var dynamodb_1 = require("./dynamodb/dynamodb");
|
|
5
|
-
Object.defineProperty(exports, "DynamoDBService", { enumerable: true, get: function () { return dynamodb_1.DynamoDBService; } });
|
|
6
|
-
var s3_1 = require("./s3/s3");
|
|
7
|
-
Object.defineProperty(exports, "S3Service", { enumerable: true, get: function () { return s3_1.S3Service; } });
|
|
8
|
-
var secrets_manager_1 = require("./secrets-manager/secrets-manager");
|
|
9
|
-
Object.defineProperty(exports, "SecretsManagerService", { enumerable: true, get: function () { return secrets_manager_1.SecretsManagerService; } });
|
|
10
|
-
var sfn_1 = require("./sfn/sfn");
|
|
11
|
-
Object.defineProperty(exports, "StepFunctionsService", { enumerable: true, get: function () { return sfn_1.StepFunctionsService; } });
|
|
12
|
-
var sns_1 = require("./sns/sns");
|
|
13
|
-
Object.defineProperty(exports, "SNSService", { enumerable: true, get: function () { return sns_1.SNSService; } });
|
|
14
|
-
var sqs_1 = require("./sqs/sqs");
|
|
15
|
-
Object.defineProperty(exports, "SQSService", { enumerable: true, get: function () { return sqs_1.SQSService; } });
|
|
16
|
-
var ssm_1 = require("./ssm/ssm");
|
|
17
|
-
Object.defineProperty(exports, "SSMService", { enumerable: true, get: function () { return ssm_1.SSMService; } });
|