@aligent/aws-wrappers 0.0.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/CLAUDE.md +172 -0
- package/README.md +286 -0
- package/docs/classes/DynamoDBService.md +353 -0
- package/docs/classes/S3Service.md +389 -0
- package/docs/classes/SNSService.md +95 -0
- package/docs/classes/SQSService.md +162 -0
- package/docs/classes/SSMService.md +157 -0
- package/docs/classes/SecretsManagerService.md +114 -0
- package/docs/classes/StepFunctionsService.md +134 -0
- package/docs/modules.md +15 -0
- package/package.json +32 -0
- package/src/dynamodb/dynamodb.d.ts +127 -0
- package/src/dynamodb/dynamodb.js +308 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +17 -0
- package/src/s3/s3.d.ts +131 -0
- package/src/s3/s3.js +244 -0
- package/src/secrets-manager/secrets-manager.d.ts +78 -0
- package/src/secrets-manager/secrets-manager.js +152 -0
- package/src/sfn/sfn.d.ts +38 -0
- package/src/sfn/sfn.js +74 -0
- package/src/sns/sns.d.ts +48 -0
- package/src/sns/sns.js +110 -0
- package/src/sqs/sqs.d.ts +60 -0
- package/src/sqs/sqs.js +134 -0
- package/src/ssm/ssm.d.ts +84 -0
- package/src/ssm/ssm.js +144 -0
- package/src/util/redact.d.ts +18 -0
- package/src/util/redact.js +29 -0
- package/src/util/truncate.d.ts +15 -0
- package/src/util/truncate.js +36 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
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/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# @aligent/aws-wrappers
|
|
2
|
+
|
|
3
|
+
Opinionated AWS SDK wrappers with Powertools logging and X-Ray tracing baked in. Each `*Service` class instantiates and instruments its underlying SDK client by default and emits a structured `logger.info` line on every operation, so consumers get a consistent, observable starting point without rewriting the same boilerplate per service.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @aligent/aws-wrappers
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Conventions
|
|
12
|
+
|
|
13
|
+
Every wrapper takes the same optional constructor options:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
new XService({ logger?, client? })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `logger` — a Powertools `Logger`. Defaults to `new Logger()`, which picks up `POWERTOOLS_SERVICE_NAME` from the environment (recommended) and otherwise falls back to Powertools' own default.
|
|
20
|
+
- `client` — a pre-configured SDK client. When omitted, the wrapper instantiates the SDK client itself and wraps it with `captureAWSv3Client` for X-Ray tracing. When supplied, the wrapper passes it through unchanged — the caller is responsible for X-Ray instrumentation.
|
|
21
|
+
|
|
22
|
+
### Log redaction
|
|
23
|
+
|
|
24
|
+
Every wrapper emits one `logger.info` line per SDK call with a per-method safe-field allowlist (omitting payloads, secret material, and PII recipient identifiers). Set `POWERTOOLS_LOG_LEVEL=DEBUG` to unlock the full SDK input in those log lines — useful for local development and incident triage. See `packages/aws-wrappers/CLAUDE.md` for the per-method allowlists currently in force.
|
|
25
|
+
|
|
26
|
+
### X-Ray outside Lambda
|
|
27
|
+
|
|
28
|
+
X-Ray's middleware throws by default when no active segment exists, which is the case for CLI scripts and local development. Set the environment variable to silence the noise:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
AWS_XRAY_CONTEXT_MISSING=IGNORE_ERROR
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## S3
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { S3Service } from '@aligent/aws-wrappers';
|
|
38
|
+
|
|
39
|
+
const s3 = new S3Service();
|
|
40
|
+
|
|
41
|
+
await s3.putObject({ Bucket: 'my-bucket', Key: 'file.txt', Body: 'hello' });
|
|
42
|
+
|
|
43
|
+
await s3.putJsonObject({ Bucket: 'my-bucket', Key: 'data.json', Body: { foo: 'bar' } });
|
|
44
|
+
const data = await s3.getJsonObject<MyType>({ Bucket: 'my-bucket', Key: 'data.json' });
|
|
45
|
+
|
|
46
|
+
// getObject returns the raw GetObjectCommandOutput when you need metadata
|
|
47
|
+
// (LastModified, ContentLength, …) alongside the body.
|
|
48
|
+
const raw = await s3.getObject({ Bucket: 'my-bucket', Key: 'file.txt' });
|
|
49
|
+
const body = await s3.getObjectBody({ Bucket: 'my-bucket', Key: 'file.txt' });
|
|
50
|
+
|
|
51
|
+
const { LastModified } = await s3.headObject({ Bucket: 'my-bucket', Key: 'file.txt' });
|
|
52
|
+
|
|
53
|
+
const keys = await s3.listObjects('my-bucket', 'prefix/');
|
|
54
|
+
const items = await s3.getAllObjects<MyType>('my-bucket', 'prefix/');
|
|
55
|
+
|
|
56
|
+
await s3.copyObject({ Bucket: 'dest', Key: 'dest-key', CopySource: 'src/src-key' });
|
|
57
|
+
|
|
58
|
+
// Presigned URLs for direct browser-side download / upload.
|
|
59
|
+
// expiresIn defaults to 3600 seconds.
|
|
60
|
+
const downloadUrl = await s3.getPresignedUrl({
|
|
61
|
+
Bucket: 'my-bucket',
|
|
62
|
+
Key: 'file.txt',
|
|
63
|
+
action: 'get',
|
|
64
|
+
});
|
|
65
|
+
const uploadUrl = await s3.getPresignedUrl({
|
|
66
|
+
Bucket: 'my-bucket',
|
|
67
|
+
Key: 'file.txt',
|
|
68
|
+
action: 'put',
|
|
69
|
+
expiresIn: 600,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await s3.deleteObject({ Bucket: 'my-bucket', Key: 'file.txt' });
|
|
73
|
+
await s3.deleteObjects('my-bucket', ['key1', 'key2']); // auto-chunked to 1000 keys per request
|
|
74
|
+
await s3.emptyBucket('my-bucket'); // streams the listing + delegates each page to deleteObjects
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Input shapes are intentionally tight (`Bucket`, `Key`, `Body` and similar). Callers needing SDK-specific options like server-side encryption or tagging should use `S3Client` directly.
|
|
78
|
+
|
|
79
|
+
## DynamoDB
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { DynamoDBService } from '@aligent/aws-wrappers';
|
|
83
|
+
|
|
84
|
+
const ddb = new DynamoDBService();
|
|
85
|
+
|
|
86
|
+
// Backed by DynamoDBDocumentClient — items are plain TS objects in both directions.
|
|
87
|
+
await ddb.putItem({ TableName: 'my-table', Item: { pk: 'abc', value: 42 } });
|
|
88
|
+
|
|
89
|
+
// Key-bearing methods take two generics: <K, R> for the key shape and the
|
|
90
|
+
// return / Attributes shape. Both default to Record<string, unknown> so callers
|
|
91
|
+
// can omit one or both when they don't care.
|
|
92
|
+
type MyKey = { pk: string };
|
|
93
|
+
type MyItem = { pk: string; value: number };
|
|
94
|
+
|
|
95
|
+
const item = await ddb.getItem<MyKey, MyItem>({
|
|
96
|
+
TableName: 'my-table',
|
|
97
|
+
Key: { pk: 'abc' },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const { Items } = await ddb.query<MyItem>({
|
|
101
|
+
TableName: 'my-table',
|
|
102
|
+
KeyConditionExpression: 'pk = :pk',
|
|
103
|
+
ExpressionAttributeValues: { ':pk': 'abc' },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// scan returns the same shape as query — full output with Items typed as T[].
|
|
107
|
+
const { Items: all } = await ddb.scan<{ pk: string }>({ TableName: 'my-table' });
|
|
108
|
+
|
|
109
|
+
// updateItem / deleteItem mirror getItem's <K, R> generics.
|
|
110
|
+
const { Attributes } = await ddb.updateItem<MyKey, { value: number }>({
|
|
111
|
+
TableName: 'my-table',
|
|
112
|
+
Key: { pk: 'abc' },
|
|
113
|
+
UpdateExpression: 'SET #v = :v',
|
|
114
|
+
ExpressionAttributeNames: { '#v': 'value' },
|
|
115
|
+
ExpressionAttributeValues: { ':v': 99 },
|
|
116
|
+
ReturnValues: 'ALL_NEW',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await ddb.deleteItem({ TableName: 'my-table', Key: { pk: 'abc' } });
|
|
120
|
+
|
|
121
|
+
// batchWrite retries UnprocessedItems with jittered exponential backoff
|
|
122
|
+
// (5 attempts, 200ms base) and throws if any remain after the final attempt.
|
|
123
|
+
await ddb.batchWrite({
|
|
124
|
+
RequestItems: {
|
|
125
|
+
'my-table': [{ PutRequest: { Item: { pk: 'abc' } } }],
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// paginateItems / paginateScan yield each item — use for potentially unbounded result sets.
|
|
130
|
+
for await (const item of ddb.paginateItems<{ pk: string }>({
|
|
131
|
+
TableName: 'my-table',
|
|
132
|
+
KeyConditionExpression: 'pk = :pk',
|
|
133
|
+
ExpressionAttributeValues: { ':pk': 'abc' },
|
|
134
|
+
})) {
|
|
135
|
+
console.log(item.pk);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for await (const item of ddb.paginateScan<{ pk: string }>({ TableName: 'my-table' })) {
|
|
139
|
+
console.log(item.pk);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`batchGet` is **not** generic — its `Responses` field is a multi-table record whose item shapes can differ per table, so no single generic can soundly describe it. Callers should narrow the result type at the call site.
|
|
144
|
+
|
|
145
|
+
## Secrets Manager
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { SecretsManagerService } from '@aligent/aws-wrappers';
|
|
149
|
+
|
|
150
|
+
const secrets = new SecretsManagerService();
|
|
151
|
+
|
|
152
|
+
const raw = await secrets.getSecret('my-secret-name');
|
|
153
|
+
const config = await secrets.getJsonSecret<MySecretShape>('my-secret-name');
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`getSecret` throws when the response has no `SecretString` (e.g. a binary-only secret). `getJsonSecret` additionally throws if the secret value is not valid JSON. `VersionId` / `VersionStage` aren't exposed — use `SecretsManagerClient` directly if you need version pinning.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// Write operations. Prefer IaC (CDK / Terraform) for secret lifecycle;
|
|
160
|
+
// reserve these for rotation flows or dynamically-issued credentials.
|
|
161
|
+
await secrets.createSecret({ Name: 'my-secret', SecretString: 'shh' });
|
|
162
|
+
await secrets.updateSecret({ SecretId: 'my-secret', Description: 'updated' });
|
|
163
|
+
await secrets.putSecretValue({ SecretId: 'my-secret', SecretString: 'new-shh' });
|
|
164
|
+
await secrets.deleteSecret({ SecretId: 'my-secret', RecoveryWindowInDays: 7 });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Step Functions
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { StepFunctionsService } from '@aligent/aws-wrappers';
|
|
171
|
+
|
|
172
|
+
const sfn = new StepFunctionsService();
|
|
173
|
+
|
|
174
|
+
// Auto-paginated — returns every execution across all pages.
|
|
175
|
+
const executions = await sfn.listExecutions({
|
|
176
|
+
stateMachineArn: 'arn:aws:states:us-east-1:0:stateMachine:my-sfn',
|
|
177
|
+
statusFilter: 'RUNNING',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const { executionArn } = await sfn.startExecution({
|
|
181
|
+
stateMachineArn: 'arn:aws:states:us-east-1:0:stateMachine:my-sfn',
|
|
182
|
+
input: JSON.stringify({ foo: 'bar' }),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const description = await sfn.describeExecution({ executionArn });
|
|
186
|
+
|
|
187
|
+
await sfn.stopExecution({ executionArn });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## SSM Parameter Store
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { SSMService } from '@aligent/aws-wrappers';
|
|
194
|
+
|
|
195
|
+
const ssm = new SSMService();
|
|
196
|
+
|
|
197
|
+
const apiKey = await ssm.getParameter('/myapp/api-key');
|
|
198
|
+
|
|
199
|
+
// Supply an alias-to-path map — the result is keyed by the aliases so the
|
|
200
|
+
// SSM path is only mentioned at the call site.
|
|
201
|
+
const { host, port } = await ssm.getParameters({
|
|
202
|
+
host: '/myapp/host',
|
|
203
|
+
port: '/myapp/port',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Auto-paginated, returns full Parameter[] (includes Version, LastModifiedDate).
|
|
207
|
+
// Recursive defaults to true.
|
|
208
|
+
const params = await ssm.getParametersByPath('/myapp/');
|
|
209
|
+
const shallow = await ssm.getParametersByPath('/myapp/', { recursive: false });
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
All read operations enable `WithDecryption` automatically — there's no opt-out. Callers needing plaintext should use `SSMClient` directly.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Write operations. Prefer IaC (CDK / Terraform) for parameter lifecycle;
|
|
216
|
+
// reserve these for values that genuinely mutate at runtime.
|
|
217
|
+
await ssm.putParameter({
|
|
218
|
+
Name: '/myapp/feature-flag',
|
|
219
|
+
Value: 'enabled',
|
|
220
|
+
Type: 'String',
|
|
221
|
+
Overwrite: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await ssm.deleteParameter('/myapp/feature-flag');
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## SQS
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { SQSService } from '@aligent/aws-wrappers';
|
|
231
|
+
|
|
232
|
+
const sqs = new SQSService();
|
|
233
|
+
|
|
234
|
+
await sqs.sendMessage({ QueueUrl, MessageBody: 'hello' });
|
|
235
|
+
|
|
236
|
+
// Returns Message[] — empty array when nothing's available.
|
|
237
|
+
const messages = await sqs.receiveMessages({ QueueUrl, WaitTimeSeconds: 20 });
|
|
238
|
+
|
|
239
|
+
for (const message of messages) {
|
|
240
|
+
if (message.ReceiptHandle) {
|
|
241
|
+
await sqs.deleteMessage({ QueueUrl, ReceiptHandle: message.ReceiptHandle });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Batch methods auto-chunk Entries to the SQS-enforced 10-entry limit.
|
|
246
|
+
await sqs.sendMessageBatch({ QueueUrl, Entries: bigEntryList });
|
|
247
|
+
await sqs.deleteMessageBatch({ QueueUrl, Entries: receiptEntries });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`receiveMessages` does **not** auto-delete — visibility-timeout semantics are the caller's responsibility.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// Opt-in truncation for oversized payloads. Defaults to off (SDK throws on
|
|
254
|
+
// oversize). Useful for fire-and-forget flows where dropped detail beats a
|
|
255
|
+
// thrown error.
|
|
256
|
+
const sqs = new SQSService({ truncate: true }); // per-instance default
|
|
257
|
+
await sqs.sendMessage({ QueueUrl, MessageBody: huge });
|
|
258
|
+
await sqs.sendMessage({ QueueUrl, MessageBody: huge }, { truncate: false }); // per-call override
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## SNS
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { SNSService } from '@aligent/aws-wrappers';
|
|
265
|
+
|
|
266
|
+
const sns = new SNSService();
|
|
267
|
+
|
|
268
|
+
await sns.publish({ TopicArn, Message: 'hello' });
|
|
269
|
+
|
|
270
|
+
// publishBatch auto-chunks PublishBatchRequestEntries to the SNS-enforced 10-entry limit.
|
|
271
|
+
await sns.publishBatch({ TopicArn, PublishBatchRequestEntries: bigEntryList });
|
|
272
|
+
|
|
273
|
+
// Opt-in truncation — same shape as SQS. Truncates Message (256 KB byte-safe)
|
|
274
|
+
// and Subject (100 chars codepoint-safe) when enabled.
|
|
275
|
+
const truncSns = new SNSService({ truncate: true });
|
|
276
|
+
await truncSns.publish({ TopicArn, Message: huge, Subject: long });
|
|
277
|
+
await truncSns.publish({ TopicArn, Message: huge }, { truncate: false });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Build / test
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
npm run build # affected only
|
|
284
|
+
npm run test
|
|
285
|
+
npm run lint
|
|
286
|
+
```
|