aws-service-stack
v0.18.383
Published
chinggis core backend package
Keywords
Readme
AWS Service Stack
📦 NPM Package: aws-service-stack
A comprehensive TypeScript backend framework for AWS serverless applications, providing enterprise-grade abstractions for DynamoDB, OpenSearch, API Gateway, Lambda, and Cognito integration.
🚀 Features
Core Infrastructure
- 🏗️ Base Controllers - Abstract AWS Lambda event handling for API Gateway, SQS, DynamoDB Streams, and WebSocket events
- 🗄️ Repository Pattern - Unified interfaces for DynamoDB and OpenSearch with transaction support
- 🔐 Authentication & Authorization - Built-in Cognito integration with role-based permissions
- 📊 Search & Analytics - Full-text search with OpenSearch integration
- ✅ Validation - Support for both Zod and Yup schemas with type-safe validation
- ✅ Permission - Item owner logic with built-in access control and parent child user management
AWS Services Integration
- DynamoDB - Complete CRUD operations, batch processing, transactions, and advanced querying
- OpenSearch - Full-text search, faceted search, and analytics
- API Gateway - RESTful API handling with automatic request/response parsing
- Lambda - Event-driven architecture with SQS, DynamoDB Streams, and scheduled events
- Cognito - User authentication, authorization, and group management
- Secrets Manager - Secure credential management
Developer Experience
- 🔧 TypeScript First - Full type safety with comprehensive interfaces
- 🏛️ Dependency Injection - TypeDI integration for clean architecture
- 📝 Comprehensive Logging - Structured logging with different levels
- 🧪 Testing Ready - Jest configuration with TypeScript support
- 📦 Modular Design - Tree-shakable modules for optimal bundle size
📦 Installation
npm install aws-service-stackPeer Dependencies
This package requires the following AWS SDK v3 packages as peer dependencies:
npm install
@aws-sdk/client-dynamodb
@aws-sdk/client-cognito-identity-provider
@aws-sdk/client-secrets-manager
@aws-sdk/util-dynamodb
@opensearch-project/opensearch🏁 Quick Start
1. Basic Controller Setup
import Container, { Service } from "typedi";
import { CONFIG_Product, path } from "./Product-config";
import { ProductService } from "../../service/product/product-service-crud.interface";
import "../../service/product/product-service-crud";
import { ControllerApi, HttpRequest } from "@chinggis/core";
import { Product } from "src/model-shared/product.model";
@Service("ProductControllerApi")
export class ProductControllerApi extends ControllerApi<Product, ProductService> {
constructor() {
const service: ProductService = Container.get("ProductCrudService");
super(service, CONFIG_PRODUCT.toObject());
}
/** Additional custom endpoints */
protected async processCrudRequest(req: HttpRequest): Promise<any> {
if (req.resourcePath === `GET ${path}/list-non-level-one`) {
return await this.service.ListNonLevelOne(req.filter);
}
}
}2. Entity Configuration
import { Access as a, DynamoIndexMap, EndpointPolicy, EntityConfigImpl, HttpMethod as m } from "@chinggis/core";
import {
CREATE,
REPLACE,
RESPONSE_FIELDS_DETAILS as FIELDS_D,
RESPONSE_FIELDS_LIST as FIELDS_L,
UPDATE,
} from "../../model-shared/product.model";
export const openSearch_Product = {
domain: "https://search-amplify-opense-abcd.ap-southeast-1.es.amazonaws.com",
index: "product",
};
export const path = "/products"; // url path base
// Product configuration
export class ProductConfig extends EntityConfigImpl {
constructor() {
// DYNAMODB
const tableName = "product-dev";
const ownerFieldName = "owner";
const indexMap = new DynamoIndexMap()
.setFields("id")
.set("byAccountType", { field: "accountType", rFields: ["accountType"] });
// PERMISSIONS
const adminGroupName = ["adminUsers"];
const policyList: EndpointPolicy[] = [
{ method: m.GET, path: `${path}`, access: [a.ADMIN], response: FIELDS_L },
{ method: m.GET, path: `${path}/search`, access: [a.ADMIN], response: FIELDS_L },
{ method: m.GET, path: `${path}/list-non-level-one`, access: [a.ADMIN], response: FIELDS_L },
{ method: m.GET, path: `${path}/{id}`, access: [a.OWNER, a.ADMIN], response: FIELDS_D },
{ method: m.POST, path: `${path}`, access: [a.ADMIN], validator: CREATE, response: FIELDS_D },
{ method: m.PUT, path: `${path}/{id}`, access: [a.ADMIN], validator: REPLACE, response: FIELDS_D },
{ method: m.PATCH, path: `${path}/{id}`, access: [a.ADMIN], validator: UPDATE, response: FIELDS_D },
{ method: m.DELETE, path: `${path}/{id}`, access: [a.ADMIN] },
];
// INIT
super(path, adminGroupName);
this.setDynamoDB(tableName, ownerFieldName, indexMap)
.setOpenSearch(openSearch_Product.domain, openSearch_Product.index)
.setPolicies(policyList);
}
}
// Export default Product configuration
export const CONFIG_PRODUCT = new ProductConfig();3. Repository Definition - DynamoDB
import { Service } from "typedi";
import { BaseRepoDBImpl } from "@chinggis/core";
import { Product } from "../../model-shared/product.model";
import { ProductRepoDB } from "./product-repo-db.interface";
import "./product-repo-db";
@Service("ProductRepoDB")
export class ProductRepoDBImpl extends BaseRepoDBImpl<Product> implements ProductRepoDB {}
/** additional custom repo methode for dynamodb integration**/
//...4. Repository Definition - OpenSearch
import { Service } from "typedi";
import { BaseRepoESImpl } from "@chinggis/core";
import { Product } from "../../model-shared/product.model";
import { ProductRepoES } from "./product-repo-es.interface";
import "./product-repo-es";
@Service("ProductRepoES")
export class ProductRepoESImpl extends BaseRepoESImpl<Product> implements ProductRepoES {}
/** additional custom repo methode for openSearch integration**/
//...5. Service Definition
import { Container, Service } from "typedi";
import { ProfileService } from "./Product-service.interface";
import "./Product-service";
import { ProfileRepoDB } from "../repositories/Product/Product-repo-db.interface";
import "../repositories/Product/Product-repo-db";
import { ErrorHttp } from "../../exception/errors";
import { CrudServiceImpl } from "@chinggis/core";
import { Product } from "../model-shared/example.model";
@Service("ProfileService")
export class ProductServiceImpl
extends CrudServiceImpl<Product, ProductRepoDB, ProductRepoES>
implements ProductService
{
constructor() {
super(Container.get("ProductRepoDB"), Container.get("ProductRepoES"));
}
/** Custom service methode **/
//...
}🛠️ Built-in API Endpoints
Your AWS Service Stack automatically provides these RESTful endpoints for any entity:
Standard CRUD Operations
POST /products # Insert new product (create)
GET /products/{id} # Get single product by ID
GET /products # List products via DynamoDB Query (by categoryId, ProductId, status, …)
GET /products/search # Search products via OpenSearch (full-text, sort, pagination)
GET /products/search/query # Search Query products via OpenSearch (aggregations, multi-filters, full-text, sort, pagination)
GET /products/scan # Scan all products (Admin only, expensive in DynamoDB)
PUT /products/{id} # Update full product (replace all fields)
PATCH /products/{id} # Update partial product (only specific fields)
DELETE /products/{id} # Delete product by IDList Operation - URL Filter
index filter, combined index filter, range filter for date or number field, sort, search, paged
openSearch: .../products/search?category=Toys&color=red&age_from=5&age_to=8&page=5&size=50
dynamoDB: .../products/?category=Toys&color=red&age_from=5&age_to=8&size=50&lastKey=dfashdfjkasd89843dfa- order=ASC
- oderBy=createdDate
- searchKeyword=marvel
- searchBy=name,manufacture,description
- [customFieldName]=[matchValue]
- size=20
- page=3 or lastKey=...
- [dateOrNumberFieldName]_from=[Value]
- [dateOrNumberFieldName]_to=[Value]
- begin_[dateOrNumberFieldName]=[Value]
- end_[dateOrNumberFieldName]=[Value]
OpenSearch V2 List Operation - URL Filter
multi-filter, aggregations, search keyword, multi-sort
openSearch v2: .../products/search/query?category__eq=Toys&color__in=red,yellow&age__gte=5&age__lte=8&page=5&size=50&agg__terms=status&agg__terms__sub=customerIdfield__in=red,yellow
field__eq=Toys
field__gte=5
field__lte=8
field__ne=Book
field__exists=true
field__exists=false
field__regex=^Foo.*
agg__terms=status
agg__terms__sub=customerId
agg__sum=price
agg__avg=price
agg__min=price
agg__max=price
search=marvel
searchFields=name,manufacture,description
searchOperator=or
searchOperator=and
sort=desc,asc
sortField=price,category
color__in=red,yellow
category__eq=Toys
status__exists=true
age__gte=5
age__lte=8
size=20
page=3
Advanced Features
- 🔍 Smart Filtering - DynamoDB queries with complex conditions
- 🔎 Full-Text Search - OpenSearch integration with faceted search
- 📄 Pagination - Built-in pagination with
lastKeysupport - 🔐 Authorization - Role-based access control per endpoint
- ✅ Validation - Automatic request/response validation
- 📊 Analytics - Built-in logging and monitoring
🏗️ Architecture
Layered Architecture
┌─────────────────┐
│ Controllers │ ← API Gateway, Lambda Events
├─────────────────┤
│ Services │ ← Business Logic
├─────────────────┤
│ Repositories │ ← Data Access (DynamoDB, OpenSearch)
├─────────────────┤
│ Providers │ ← AWS SDK Clients
└─────────────────┘Event Handling
- API Gateway - RESTful API endpoints
- SQS - Message queue processing
- DynamoDB Streams - Real-time data processing
- WebSocket - Real-time communication
- Scheduled Events - Cron-based tasks
🔧 Configuration
Environment Variables
# AWS Configuration
AWS_REGION=us-east-1
IAM_ACCESS_KEY=your-access-key
IAM_SECRET_ACCESS_KEY=your-secret-key
# OpenSearch
OPENSEARCH_ENDPOINT=https://your-domain.us-east-1.es.amazonaws.com
# API Gateway
WS_ENDPOINT=wss://your-websocket-api.execute-api.us-east-1.amazonaws.com/prodTypeScript Configuration
// tsconfig.json
{
"compilerOptions"
:
{
"baseUrl"
:
"./",
"paths"
:
{
"@chinggis/core"
:
["node_modules/aws-service-stack/dist/index.d.ts"],
"@chinggis/types"
:
["node_modules/aws-service-stack/dist/model/index.d.ts"]
}
}
}📚 API Reference
BaseController
handleRequest(event)- Main Lambda handlerhandleSQS(event)- SQS message processinghandleWebSocket(event)- WebSocket connection handling
BaseService
save(entity, ownerId)- Create/update entityfindById(id, userId)- Find entity by IDfind(filter: Filter, userId)- Find entity by filterdelete(id, userId)- Delete entity...- Usefully crud methods
Repository Interfaces
CoreRepo<T>- Generic CRUD operationsBaseRepoDB<T>- DynamoDB-specific operationsBaseRepoES<T>- OpenSearch-specific operations
🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run specific test file
npm test -- user.test.ts🚀 Example Deployment
Serverless Framework - For Example
# serverless.yml
service: my-aws-service
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
functions:
api:
handler: dist/handler.handler
events:
- http:
path: /{proxy+}
method: ANYAWS CDK
import { Function, Runtime } from "aws-cdk-lib/aws-lambda";
new Function(this, "MyFunction", {
runtime: Runtime.NODEJS_18_X,
handler: "dist/handler.handler",
code: Code.fromAsset("dist"),
});🔐 Role-Based Access Control (RBAC)
The framework provides a centralized RBAC system through ControllerRole, which manages Cognito user pool groups, DynamoDB-backed permissions, and scope-based data filtering — all integrated directly into ControllerApi.
Architecture
┌──────────────────┐
│ ControllerApi │ ← Delegates RBAC check automatically
├──────────────────┤
│ ControllerRole │ ← Permission CRUD, Cognito groups, scope enforcement
├──────────────────┤
│PermissionService │ ← Read-through cache (15-min TTL) + orchestration
├──────────────────┤
│ PermissionRepo │ ← DynamoDB via BaseRepoDBImpl
└──────────────────┘Permission Model
Each permission is stored in DynamoDB with the composite key role#resource#scope:
interface Permission {
id: string;
role: string; // Cognito group name: "Manager", "Agent"
resource: string; // API resource: "property", "order"
scope: string; // Data scope: "Organization", "Branch", "Agent"
method: {
get?: boolean;
post?: boolean;
patch?: boolean;
put?: boolean;
delete?: boolean;
};
permissionKey: string; // "Manager#property#Branch"
}Configuration
Enable RBAC by adding ROLE_TABLE, ROLE_PATH, and SCOPE_MAP to your entity config:
import { EntityConfigImpl, ScopeMap } from "@chinggis/core";
const scopeMap: ScopeMap = new Map([
["Organization", { filterField: "orgId", claimKey: "custom:orgId" }],
["Branch", { filterField: "branchId", claimKey: "custom:branch" }],
["Agent", { filterField: "agentId", claimKey: "custom:agent" }],
]);
export class PropertyConfig extends EntityConfigImpl {
constructor() {
super("/properties", ["adminUsers"]);
this.setDynamoDB("property-dev", "owner", indexMap)
.setOpenSearch(domain, "property")
.setPolicies(policyList)
.setScopes(scopeMap)
.setRoleTable("role-permissions-dev")
.setRolePath("/properties/role");
}
}When both ROLE_TABLE and SCOPE_MAP are configured, ControllerApi automatically creates a ControllerRole instance and enforces RBAC on every request.
How It Works
- Request arrives at
ControllerApi.resolveCrudRequest() - If RBAC is configured,
ControllerRole.checkRbacAccess()is called - User's role is extracted from JWT
groups[0] - Scope is read from
?scope=Branchquery parameter and validated againstScopeMap - Permission is checked via
PermissionService(with in-memory cache) - On success, scope filter is applied (e.g.,
filter.branchId = identity["custom:branch"]) - On failure,
403 PermissionDeniedis thrown
Permission CRUD Endpoints
When ROLE_PATH is configured, permission management endpoints are automatically available:
GET /properties/role # List all permissions
POST /properties/role # Create a permission
PATCH /properties/role # Update a permission
DELETE /properties/role # Delete a permission
POST /properties/role/add-role # Create a Cognito user pool group
POST /properties/role/add-user-role # Add a User to cognito groupCreate a Permission
POST /properties/role
{
"role": "Manager",
"resource": "property",
"scope": "Branch",
"method": { "get": true, "post": true, "patch": true, "delete": false }
}Create a Cognito Group (Role)
POST /properties/role/add-role
{
"groupName": "Manager",
"description": "Branch-level managers"
}Programmatic Usage
ControllerRole can also be used directly in custom controllers or services:
import { ControllerRole } from "@chinggis/core";
const roleController = new ControllerRole("role-permissions-dev");
// Permission CRUD
await roleController.addPermission({
role: "Manager",
resource: "property",
scope: "Branch",
method: { get: true, post: true, patch: true },
});
await roleController.listPermissions();
await roleController.updatePermission("perm-id", { method: { delete: true } });
await roleController.deletePermission("perm-id");
// Permission check
const allowed = await roleController.hasPermission("Manager", "property", "Branch", "GET");
// Cognito group management
await roleController.addRole("us-east-1_PoolId", "Manager", "Branch-level managers");
await roleController.assignRole("us-east-1_PoolId", "[email protected]", "Manager");Caching
PermissionService uses a read-through cache with 15-minute TTL optimized for Lambda warm invocations:
- Cache hits return instantly without DB calls
- Concurrent requests for the same key are deduplicated (single in-flight fetch)
- Cache is automatically invalidated on create, update, and delete operations
🤝 Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat(core): add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
# Clone the repository
git clone https://github.com/chinggis-systems/aws-service-stack.git
# Install dependencies
npm install
# Run tests
npm test
# Build the project
npm run build📄 License
This project is licensed under the ISC License - see the LICENSE file for details.
🏢 About Chinggis Systems
Built by Chinggis Systems - Enterprise software solutions for modern cloud architectures.
📞 Support
- 💬 Discord: Join our community
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Documentation: Wiki
Made with ❤️ for the AWS community
