@aligent/aws-wrappers 0.1.0 → 0.1.2

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.
Files changed (39) hide show
  1. package/cjs/index.cjs +1911 -0
  2. package/cjs/index.d.ts +1 -0
  3. package/cjs/package.json +51 -0
  4. package/cjs/src/index.d.ts +7 -0
  5. package/esm/index.d.ts +1 -0
  6. package/esm/index.mjs +1903 -0
  7. package/esm/package.json +51 -0
  8. package/esm/src/dynamodb/dynamodb.d.ts +127 -0
  9. package/esm/src/index.d.ts +7 -0
  10. package/esm/src/s3/s3.d.ts +131 -0
  11. package/esm/src/secrets-manager/secrets-manager.d.ts +78 -0
  12. package/esm/src/sfn/sfn.d.ts +38 -0
  13. package/esm/src/sns/sns.d.ts +48 -0
  14. package/esm/src/sqs/sqs.d.ts +60 -0
  15. package/esm/src/ssm/ssm.d.ts +84 -0
  16. package/{src/util/redact.js → esm/src/util/redact.d.ts} +2 -13
  17. package/esm/src/util/truncate.d.ts +15 -0
  18. package/package.json +35 -17
  19. package/CLAUDE.md +0 -173
  20. package/src/dynamodb/dynamodb.js +0 -308
  21. package/src/index.d.ts +0 -7
  22. package/src/index.js +0 -17
  23. package/src/s3/s3.js +0 -244
  24. package/src/secrets-manager/secrets-manager.js +0 -152
  25. package/src/sfn/sfn.js +0 -74
  26. package/src/sns/sns.js +0 -110
  27. package/src/sqs/sqs.js +0 -134
  28. package/src/ssm/ssm.js +0 -144
  29. package/src/util/truncate.js +0 -36
  30. package/tsconfig.lib.tsbuildinfo +0 -1
  31. /package/{src → cjs/src}/dynamodb/dynamodb.d.ts +0 -0
  32. /package/{src → cjs/src}/s3/s3.d.ts +0 -0
  33. /package/{src → cjs/src}/secrets-manager/secrets-manager.d.ts +0 -0
  34. /package/{src → cjs/src}/sfn/sfn.d.ts +0 -0
  35. /package/{src → cjs/src}/sns/sns.d.ts +0 -0
  36. /package/{src → cjs/src}/sqs/sqs.d.ts +0 -0
  37. /package/{src → cjs/src}/ssm/ssm.d.ts +0 -0
  38. /package/{src → cjs/src}/util/redact.d.ts +0 -0
  39. /package/{src → cjs/src}/util/truncate.d.ts +0 -0
package/package.json CHANGED
@@ -1,32 +1,50 @@
1
1
  {
2
2
  "name": "@aligent/aws-wrappers",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Opinionated AWS SDK wrappers with Powertools logging and X-Ray tracing",
5
- "type": "commonjs",
6
- "main": "./src/index.js",
7
- "typings": "./src/index.d.ts",
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",
11
30
  "directory": "packages/aws-wrappers"
12
31
  },
13
32
  "dependencies": {
14
- "@aws-lambda-powertools/logger": "^2.33.0",
15
- "@aws-sdk/client-dynamodb": "^3.1045.0",
16
- "@aws-sdk/client-s3": "^3.1045.0",
17
- "@aws-sdk/client-secrets-manager": "^3.1045.0",
18
- "@aws-sdk/client-sfn": "^3.1045.0",
19
- "@aws-sdk/client-sns": "^3.1045.0",
20
- "@aws-sdk/client-sqs": "^3.1045.0",
21
- "@aws-sdk/client-ssm": "^3.1045.0",
22
- "@aws-sdk/lib-dynamodb": "^3.1045.0",
23
- "@aws-sdk/s3-request-presigner": "^3.1045.0",
33
+ "@aws-lambda-powertools/logger": "^2.33.1",
34
+ "@aws-sdk/client-dynamodb": "^3.1068.0",
35
+ "@aws-sdk/client-s3": "^3.1068.0",
36
+ "@aws-sdk/client-secrets-manager": "^3.1068.0",
37
+ "@aws-sdk/client-sfn": "^3.1068.0",
38
+ "@aws-sdk/client-sns": "^3.1068.0",
39
+ "@aws-sdk/client-sqs": "^3.1068.0",
40
+ "@aws-sdk/client-ssm": "^3.1068.0",
41
+ "@aws-sdk/lib-dynamodb": "^3.1068.0",
42
+ "@aws-sdk/s3-request-presigner": "^3.1068.0",
24
43
  "aws-xray-sdk-core": "^3.12.0"
25
44
  },
26
45
  "author": "Aligent",
27
46
  "license": "MIT",
28
47
  "devDependencies": {
29
48
  "aws-sdk-client-mock": "^4.1.0"
30
- },
31
- "types": "./src/index.d.ts"
32
- }
49
+ }
50
+ }
package/CLAUDE.md DELETED
@@ -1,173 +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?: LoggerInterface; client?: <ServiceClient> })
35
- ```
36
-
37
- - `logger` is typed as `LoggerInterface` (from `@aws-lambda-powertools/logger/types`), **not** the concrete `Logger` class. This avoids the dual-package hazard for ESM consumers — TS treats the ESM and CJS builds of the `Logger` class as nominally distinct (each has its own `#private` field), but `LoggerInterface` is a structural type alias so both builds are mutually assignable. The wrapper still defaults to `new Logger()` internally; only the public type widens.
38
- - `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.
39
- - `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.
40
- - No `clientConfig` / `region` / `endpoint` options. Callers needing those construct their own client and pass it via `client`.
41
-
42
- ### Logging
43
-
44
- Every public method emits exactly one `logger.info('<verb> <noun>', { input })` line at the start. The shape of `input` is **level-driven**:
45
-
46
- - 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.
47
- - At any other level, only a per-method **safe-fields allowlist** is logged.
48
-
49
- The mechanism is `filterFieldsForLogLevel(logger, input, SAFE_FIELDS)` in `src/util/redact.ts`. Internal-only helper, not exported from `src/index.ts`.
50
-
51
- #### Conventions for the allowlist
52
-
53
- - **Module-level constant per method**, typed as `ReadonlyArray<keyof CommandInput>`, named `<METHOD>_SAFE_FIELDS`. TSDoc explains what's omitted and why.
54
- - **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.
55
- - **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.
56
- - **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.
57
-
58
- #### Currently redacted
59
-
60
- - **S3** — `putObject` / `putJsonObject` omit `Body`. Batch methods (`deleteObjects`, `emptyBucket`) log `{ bucket, keyCount }`.
61
- - **SNS** — `publish` omits `Message`, `Subject`, `MessageAttributes`, `PhoneNumber`. `publishBatch` logs `{ TopicArn, entryCount }`.
62
- - **SQS** — `sendMessage` omits `MessageBody`, `MessageAttributes`. Batch methods log `{ QueueUrl, entryCount }`.
63
- - **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.
64
- - **SecretsManager** — write methods omit `SecretString` / `SecretBinary`.
65
- - **SSM** — `putParameter` omits `Value`.
66
- - **SFN** — `startExecution` omits the execution `input` (often carries PII).
67
-
68
- 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.
69
-
70
- ### Patterns
71
-
72
- - **Auto-pagination, flat array**: `S3.listObjects` / `getAllObjects` / `emptyBucket`, `SSM.getParametersByPath`, `SFN.listExecutions`. Used when the result set is bounded in practice.
73
- - **Generator pagination, yield items**: `DynamoDB.paginateItems` / `paginateScan`. Used when the result set is potentially unbounded — peak memory stays bounded by one page.
74
- - **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.
75
-
76
- ### Opt-in payload truncation
77
-
78
- `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.
79
-
80
- - Constructor option `truncate?: boolean` sets the per-instance default.
81
- - Per-call option `{ truncate?: boolean }` overrides the instance default.
82
- - 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).
83
- - Each truncating call emits a single `logger.warn('Truncated <service> <op> input', { fields: [...] })` line listing what was modified.
84
-
85
- If you find yourself adding truncation to a third method, raise it with the user first — opt-out flags are the more conservative default.
86
-
87
- ### DynamoDB specifics
88
-
89
- - Backed by `DynamoDBDocumentClient`. Always wrap the base `DynamoDBClient` with `captureAWSv3Client` **before** passing it to `DynamoDBDocumentClient.from(...)`, so X-Ray captures every command.
90
- - `marshallOptions: { removeUndefinedValues: true }`.
91
- - 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.
92
- - Generic typing convention:
93
- - Methods that return a single item or yield items (`paginateItems`, `paginateScan`): `AsyncGenerator<T>`.
94
- - Methods that return the full command output (`query`, `scan`): preserve the output and generically type only the data-bearing field (`Items?: T[]`).
95
- - 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.
96
- - `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.
97
- - All generics default to `Record<string, unknown>` so callers can omit them.
98
-
99
- ## Adding a new service
100
-
101
- Walk through these in order. Each step is small; nothing is automatable enough to be worth a generator.
102
-
103
- 1. **Confirm scope with the user.** Run the questions in the next section before writing code. Don't infer the answers from the SDK.
104
- 2. **Install the SDK client** as a runtime dependency of the package:
105
- ```sh
106
- npm install --workspace=@aligent/aws-wrappers @aws-sdk/client-<service>
107
- ```
108
- 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.
109
- 3. **Create the folder**: `packages/aws-wrappers/src/<service>/`, with `<service>.ts` and `<service>.test.ts`.
110
- 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.
111
- 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.
112
- 6. **Add a named export to `src/index.ts`** in alphabetical order.
113
- 7. **Run** `npx nx run aws-wrappers:lint --fix`, `:typecheck`, `:test --coverage`, `:typedoc`. The 80% workspace coverage threshold is enforced.
114
- 8. **Update the package `README.md`** with a worked example under a new `## <Service Name>` section.
115
- 9. **Commit** with the active ticket prefix.
116
- 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.
117
-
118
- ## Questions to ask the user
119
-
120
- ### When adding a new service
121
-
122
- Don't write code until each of these is answered. Defaults in **bold**.
123
-
124
- - **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.
125
- - **Pagination**: for each `List*` / `Get*ByPath` / etc. operation:
126
- - Auto-paginate and return a flat array? (**default** when result set is bounded by filters / retention)
127
- - Expose as an `AsyncGenerator` that yields items? (**default** when result set is potentially unbounded)
128
- - Pass-through, leave pagination to caller? (only when the caller usually wants pagination metadata)
129
- - **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?
130
- - **Generic typing**:
131
- - For methods that return a single data-bearing item — return generic `T | undefined` (**default**).
132
- - For methods that return command output with metadata — preserve the output, generic on the data-bearing field (`Items?: T[]`, `Attributes?: T`).
133
- - For multi-shape responses (cf. DynamoDB `batchGet`) — **not generic**, document why in TSDoc.
134
- - Default the generic to `T extends Record<string, unknown> = Record<string, unknown>` so callers can omit it.
135
- - **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 }`.
136
- - **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`).
137
- - **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.
138
- - **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.
139
-
140
- ### When changing an existing service
141
-
142
- - Is this change additive (new method) or modifying an existing public method?
143
- - If modifying: is the package already published? If yes, this is a SemVer-major change — flag it before implementing.
144
- - Does it touch any of the locked-in conventions above? If yes, surface to the user first.
145
- - 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.
146
-
147
- 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.
148
-
149
- ## Testing notes
150
-
151
- - `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:**
152
-
153
- ```ts
154
- const mock = mockClient(SSMClient); // global intercept
155
- const service = new SSMService({ client: new SSMClient({}) }); // real instance
156
- ```
157
-
158
- 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.
159
-
160
- - 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.
161
-
162
- - 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.
163
-
164
- ## Out of scope
165
-
166
- These came up during the initial design and were explicitly deferred:
167
-
168
- - Moving the existing AWS utilities (`S3Dao`, `fetchSsmParams`, `get-aws-id-from-arn`) out of `microservice-util-lib` into this package.
169
- - Deprecating those existing utilities or adding `@deprecated` tags.
170
- - Migration guide from the legacy utilities to the new wrappers.
171
- - Integration tests against LocalStack or real AWS.
172
- - CDK constructs in `nx-cdk` corresponding to these wrappers.
173
- - Wrappers for additional AWS services (EventBridge, Kinesis, etc.) — add them as separate tickets following the "Adding a new service" flow.
@@ -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; } });