@worktif/purews
Version:
Work TIF TypeScript-based AWS infrastructure toolkit featuring DynamoDB integration, AppSync support, SES functionality, and GraphQL capabilities with comprehensive audit logging and AWS Signature V4 authentication.
385 lines (301 loc) • 14 kB
Markdown
# @worktif/purews
Enterprise-grade TypeScript utilities for AWS-centric serverless systems. Provides a composable DI-powered bundle with batteries-included services for DynamoDB, AppSync (GraphQL), SES, S3, Lambda middy middlewares, Zod validation, and environment configuration via Zod.
[](https://www.npmjs.com/package/@worktif/purews)



---
## Overview
`@worktif/purews` is a production-ready utility toolkit designed for critical workloads (financial, cloud-native, distributed systems). It standardizes how you wire AWS services, validate configuration, instrument Lambdas, and execute GraphQL/DynamoDB/S3/SES operations with consistent logging and error semantics.
It emphasizes:
- Strong typing with TypeScript
- Deterministic environment/config validation (Zod)
- Inversion of control and factory-based DI
- Reliable logging + structured middlewares
- Low-overhead, testable service classes
---
## Key Features
- Dependency Injection Bundle
- Pure container with explicit bindings for env/config and AWS services
- Configuration
- Zod-validated env schema for AWS AppSync and SES; dotenv support
- AWS Services
- `DynamoDbService (abstract)`: typed CRUD helpers, update expression generator
- `AppSyncService`: typed GraphQL query/mutation with serializer hooks
- `SesService`: verify and send emails, verified-address checks
- `S3Service`: safe object read to string
- Lambda Utilities
- middy-based handlers for API Gateway and SQS
- Request parsing, logger injection, request-id correlation, Zod middleware
- Decorators: API injection, validation injector
- GraphQL Utils
- Types for internal GraphQL shapes; helpers for enum/case transformations
- Serialization
- Composable serializer for API and GraphQL payloads
- Observability
- Consistent logging via AWS Powertools logger with contextual metadata
## Installation
```shell
# npm
npm install @worktif/purews
# yarn
yarn add @worktif/purews
```
Peer assumptions:
- [Node.js](https://nodejs.org/en) >= 20
- [TypeScript 5.8.x](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html) recommended
- [AWS credentials](https://aws.amazon.com/iam/) available via environment or [IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html)
---
## Usage
### Basic bootstrapping
```typescript
import 'reflect-metadata';
import { bundlePurews } from '@worktif/purews';
// Access DI-provisioned services
const { appSync, ses, dynamoDb } = bundlePurews.aws.services;
// Access validated environment
const env = bundlePurews.env;
console.log(env.aws.credentials.region);
```
### AppSync GraphQL query
```typescript
import { AppSyncService } from '@worktif/purews';
const appSync = bundlePurews.aws.services.appSync as AppSyncService;
const query = `
query GetItem($id: ID!) {
getItem(id: $id) {
id
name
updatedAt
}
}
`;
const result = await appSync.query<{ id: string; name: string; updatedAt: string }>({
query,
payload: { id: 'abc-123' }, // Variables
});
console.log(result.data); // { id, name, updatedAt }
```
### AppSync GraphQL mutation with serializer
```typescript
import { identityToNull, identityToVoid } from '@worktif/utils';
const mutation = `
mutation UpdateItem($input: UpdateItemInput!, $condition: ModelItemConditionInput) {
updateItem(input: $input, condition: $condition) {
id
name
updatedAt
}
}
`;
const updated = await appSync.mutate<{ id: string; name: string }>({
mutation,
payload: { id: 'abc-123', name: 'New Name' },
}, {
serializer: identityToNull,
deserializer: identityToVoid,
});
console.log(updated.data.updateItem);
```
### SES: send email
```typescript
import { SesService } from '@worktif/purews';
const ses = bundlePurews.aws.services.ses as SesService;
await ses.sendEmail('<b>Hello</b> from PureWS', 'recipient@example.com', 'Subject');
```
### S3: read object
```typescript
import { S3Service } from '@worktif/purews';
const s3 = new S3Service(bundlePurews.env);
const content = await s3.getS3ObjectContent({ Bucket: 'my-bucket', Key: 'path/to/file.txt' });
console.log(content);
```
### Lambda API handler with middy
```typescript
import { lambdaApi } from '@worktif/purews';
type Payload = { id: string };
export const handler = lambdaApi<Payload, 'post'>(
async (event, context, internal) => {
internal?.log?.now({ body: event.body }, { message: 'Handling POST' });
return {
statusCode: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ok: true, id: event.body.id }),
};
},
{
logger: { service: { entity: 'Example', name: 'CreateItem' } },
}
);
```
### Validation decorator and middleware
```typescript
import { z } from 'zod';
import { injectApi, injectValidation } from '@worktif/purews';
import { lambdaApi } from '@worktif/purews';
const BodySchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});
const withValidation = injectValidation({ body: BodySchema });
export const handler = lambdaApi<{ email: string; name: string }, 'post'>(
async (event) => {
// event.body is typed and validated
return {
statusCode: 201,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ created: true }),
};
},
{ logger: { service: { entity: 'Users', name: 'CreateUser' } } }
).before(withValidation);
```
---
## API Reference
- Bundle and DI
- `class BundlePurews`
- `aws.services: { dynamoDb: DynamoDbService, appSync: AppSyncService, ses: SesService }`
- `env: EnvConfigPurews`
- Environment Config
- `class EnvConfigPurews extends EnvConfigDefault`
- `aws.appSync: { appId?: string; apiKey?: string; appUrl?: string; appRealTimeUrl?: string }`
- `aws.ses: { notificationEmail?: string }`
- Validates from process.env:
- `AWS_APP_SYNC_APP_ID,`
- `AWS_APP_SYNC_API_KEY,`
- `AWS_APP_SYNC_APP_URL,`
- `AWS_APP_SYNC_APP_REAL_TIME_URL,`
- `SES_NOTIFICATION_EMAIL`
- `DynamoDbService (abstract)` – Service is development
- `tableName: string`
- `put<T>(payload: Partial<DynamoEntity<T>>): Promise<T | undefined>`
- `get<T>(id: string): Promise<T | undefined>`
- `update<T extends DynamoEntity<object>>(payload: Partial<DynamoEntity<T>>): Promise<T | undefined>`
- `delete<T extends DynamoEntity<object>>(payload: Partial<T>): Promise<Partial<T> | undefined>`
- `queryByAttribute<T>(options: QueryByAttrOptions): Promise<T[] | undefined>`
- `scanFinder<T>(): Promise<T[] | undefined>`
- `static generateExpressionForUpdate<T>(payload, listAttributes?): { updateExpression: string; expressionAttributeValues: Record<string, unknown> }`
- Types:
- `DynamoEntity<T> = T & { id: string; createdAt?: string | null; updatedAt?: string | null }`
- `AppSyncService`
- `query<TResult, TVariables>(args: { query: string; payload: TVariables }, serializer?: GraphQlSerializer): Promise<GqlQueryResult<TResult>>`
- `mutate<TResult, TVariables>(args: { mutation: string; payload: TVariables; conditions?: ModelConditionInput<TResult> }, serializer?: GraphQlSerializer): Promise<GqlMutationResult<TResult>>`
- `GraphQlSerializer: { serializer?: (variables) => any; deserializer?: (result) => any }`
- Internals:
- parses input variable names and output key from DocumentNode
- `SesService`
- `sendEmail(messageHtml: string, toAddress: string, subject?: string): Promise<SendEmailCommandOutput>`
- `verifyEmailAddress(email: string): Promise<void>`
- `isEmailAddressVerified(email: string): Promise<boolean>`
- `S3Service`
- `getS3ObjectContent({ Bucket, Key }: { Bucket: string; Key: string }): Promise<string | undefined>`
- Lambda utilities
- `lambdaApi<T, TMethod>(handler, internalContext?, schema?): Middy handler for API Gateway`
- `lambdaSqs<T>(handler, internalContext?): Middy handler for SQS`
- `injectApi(injectFunction, injectCatchFunction?): MethodDecorator to pre-parse body and handle catches`
- `injectValidation(schemas, service?): returns function(event, context) to validate body/path with Zod`
- `zodValidationMiddleware(schema): middy before middleware that validates event.body`
- Types:
- `LambdaEventApi<T, TMethod>,`
- `LambdaEventMethod,`
- `LambdaEventSqs<T>,`
- `ValidationService`
---
## Use Cases
- Payments/Financial Services
- Deterministic GraphQL mutations to AppSync with safe serializers; strict audit fields on Dynamo entities
- Multi-tenant SaaS
- Composable DI container to wire per-tenant configuration sources; consistent SES notifications and verification
- Data Pipelines
- SQS consumer lambdas using lambdaSqs with uniform logging, Zod validation per record schema
- API Gateways at scale
- lambdaApi with standard parsing, logging context injection, error wrapping; easy adoption of Zod validation middleware
- Regulated environments
- Zod-validated env eliminates misconfiguration class; serializer boundaries for IO control; consistent logger metadata
---
## Design Principles
- Type-first: all public APIs are strongly typed, including GraphQL and Dynamo helpers
- Composability: DI container + service factories to swap/test components
- Functional boundaries: pure serializers, explicit side-effect services
- Predictable IO: Zod validation at boundaries; identity serializers as defaults
- Low-latency: AWS SDK v3 usage, disabled AppSync offline cache, lightweight middlewares
- Observability: structured logs with contextual service names and stages
---
## Best Practices
- Export concrete DynamoDB services by extending DynamoDbService and setting tableName
- Always validate env in CI: boot bundle in a smoke test to fail-fast on missing vars
- Keep GraphQL operations typed with TResult and TVariables; use serializer to normalize cases
- Use lambdaApi for all API Gateway handlers to standardize parsing/logging
- Keep SES notificationEmail verified; call verifyEmailAddress in provisioning workflows
---
## Compatibility and Performance
- Node.js: 18+ recommended
- AppSync: AUTH_TYPE.API_KEY supported; pluggable if you extend for Cognito/JWT
- DynamoDB: AWS SDK v3 (lib-dynamodb); uses DocumentClient marshalling with removeUndefinedValues
- Performance notes:
- Query/mutate configured with no cache thrash and typenames disabled
- Dynamo batch/get/update paths minimize marshalling overhead
- Middy handlers inject logger once, avoiding repeated construction
## Contributing
This section is intended for external publishers responsible for releasing the package to npm. Follow the sequence precisely to ensure auditability, semantic versioning integrity, and a clean release trail.
- Authenticate to the scoped registry
- `npm login --scope=@worktif`
- If you encounter a TLS/registry error, set the registry explicitly:
- `npm config set registry https://registry.npmjs.org/`
- Complete your enhancement
- Implement and locally validate your changes (types, build, docs as applicable).
- Open a Pull Request (PR)
- Submit your changes for review.
- Await approval before proceeding.
- Merge the PR
- After approval, merge into main using your standard merge policy.
- Synchronize your local main
- `git checkout main`
- `git pull` to ensure you’re up to date.
- Prepare a release branch
- Create a branch using the release template:
- `releases/v[your.semantic.version-[pre+[meta]]]-next-release-description`
- Bump the version
- Update the package version according to SemVer (major/minor/patch).
- Commit the version bump to the release branch
- Commit only the version change (and any generated artifacts if required by your policy).
- Push the release branch
- Push the branch to the remote to trigger any CI gates.
- Open a Release PR
- Create a PR from the release branch to main.
- Await approval and required checks.
- Merge the Release PR
- Merge into main after approvals and passing checks.
- Final synchronization
- Pull the latest changes from main locally.
- Validate the version in package.json
- Ensure the version reflects the intended release.
- Publish
- If the version was not increased (npm will reject):
- Bump the version, commit, and then run yarn run publish:npm.
- If the version has been increased and publishing fails unexpectedly:
- Contact the maintainer at raman@worktif.com with context (command output, Node/npm versions, CI logs).
Successful publish output resembles:
```shell
+ @worktif/purews@[your.semantic.version-[pre+[meta]]]
✨ Done in 28.81s.
```
Security and responsible disclosure
- Do not commit secrets
- Do not include secrets in tests or examples
- Report vulnerabilities privately to the maintainers contact below
- Use .npmrc with correct registry config for scoped publish
- New services must include error translation via CustomException and structured logs
---
## License
This project is licensed under the [Elastic License 2.0](https://raw.githubusercontent.com/elastic/elasticsearch/v7.16.3/licenses/ELASTIC-LICENSE-2.0.txt).
- See `LICENSE` for the full license text.
- See `NOTICE` for attribution and relicensing details (re-licensed from BUSL-1.1 on 2025-09-15).
- See `THIRD_PARTY_LICENSES.txt` for third-party attributions and license texts.
Rationale:
- Suitable for commercial use with restrictions on offering as a managed service; fit for enterprise deployments requiring source access with guardrails.
---
## Maintainers / Contact
- Maintainer: Raman Marozau, [raman@worktif.com](mailto:raman@worktif.com)
- Documentation and support: `docs/` generated via TypeDoc
For security reports, please contact support@worktif.com.