UNPKG

@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
# @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. [![npm version](https://img.shields.io/npm/v/@worktif/purews.svg)](https://www.npmjs.com/package/@worktif/purews) ![Node Support](https://img.shields.io/badge/node-%3E%3D20.0.0-43853d) ![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-3178c6) ![License](https://img.shields.io/badge/license-Elastic--2.0-blue.svg) --- ## 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.