Creating Reusable Modules
Learn how to create reusable modules in Stack9 that can be shared across projects or distributed to other teams. This guide covers module structure, creating reusable entities, queries, action types, module dependencies, versioning, packaging, and comprehensive documentation.
What You'll Learn
- ✅ Module structure and organization
- ✅ Creating reusable entities
- ✅ Creating reusable queries
- ✅ Creating reusable action types
- ✅ Managing module dependencies
- ✅ Module versioning strategies
- ✅ Packaging and distribution
- ✅ Writing module documentation
Time Required: 60-90 minutes
Prerequisites
- Completed Building a CRUD Application guide
- Understanding of Implementing Business Logic
- Familiarity with npm packages
- Basic TypeScript knowledge
Understanding Stack9 Modules
Modules in Stack9 are self-contained, reusable packages that encapsulate:
- Entities and their relationships
- Queries and data access patterns
- Action types and business logic
- Connectors and integrations
- Screens and UI components
- Documentation and examples
Why Create Modules?
| Benefit | Description |
|---|---|
| Reusability | Use the same code across multiple projects |
| Consistency | Standardize implementations across teams |
| Maintainability | Update once, benefit everywhere |
| Distribution | Share with community or sell commercially |
| Faster Development | Bootstrap new projects quickly |
Step 1: Module Structure
Standard Module Directory Structure
@stack9/my-module/
├── package.json
├── README.md
├── LICENSE
├── tsconfig.json
├── src/
│ ├── index.ts
│ ├── entities/
│ │ ├── customer.json
│ │ ├── order.json
│ │ └── product.json
│ ├── queries/
│ │ ├── getcustomerlist.json
│ │ ├── getorderdetails.json
│ │ └── searchproducts.json
│ ├── action-types/
│ │ ├── sendWelcomeEmail.ts
│ │ ├── processOrder.ts
│ │ └── updateInventory.ts
│ ├── entity-hooks/
│ │ ├── customer.vat.ts
│ │ └── order.vat.ts
│ ├── connectors/
│ │ └── email_service.json
│ ├── screens/
│ │ ├── customer_list.json
│ │ └── order_detail.json
│ ├── types/
│ │ └── index.ts
│ └── utils/
│ └── helpers.ts
├── examples/
│ ├── basic-usage.md
│ └── advanced-usage.md
└── docs/
├── api.md
├── entities.md
└── configuration.md
Step 2: Initialize a Module Project
Create package.json
File: package.json
{
"name": "@stack9/ecommerce-module",
"version": "1.0.0",
"description": "Reusable e-commerce module for Stack9 with customers, products, and orders",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": [
"stack9",
"ecommerce",
"module",
"customers",
"orders",
"products"
],
"author": "Your Name <your.email@example.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/stack9-ecommerce-module"
},
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint src/**/*.ts",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@april9/stack9-sdk": "^2.0.0"
},
"devDependencies": {
"@april9/stack9-sdk": "^2.0.0",
"@types/node": "^18.0.0",
"typescript": "^5.0.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
},
"files": [
"dist",
"src",
"README.md",
"LICENSE"
],
"stack9": {
"module": true,
"version": "1.0.0",
"compatibleWith": ">=2.0.0"
}
}
Create TypeScript Configuration
File: tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Step 3: Creating Reusable Entities
Entity Design Principles
- Generic and Flexible - Avoid project-specific fields
- Well-Documented - Clear descriptions and examples
- Extensible - Allow consumers to extend entities
- Minimal Dependencies - Limit external references
Example: Customer Entity
File: src/entities/customer.json
{
"head": {
"name": "Customer",
"key": "customer",
"pluralisedName": "customers",
"icon": "UserOutlined",
"allowComments": true,
"allowTasks": true,
"allowAttachments": false,
"isActive": true,
"description": "Customer entity for e-commerce module"
},
"fields": [
{
"label": "Customer Number",
"key": "customer_number",
"type": "TextField",
"placeholder": "CUST-001",
"description": "Unique customer identifier",
"validateRules": {
"required": true,
"maxLength": 50
},
"behaviourOptions": {
"readOnly": true
},
"index": true
},
{
"label": "First Name",
"key": "first_name",
"type": "TextField",
"placeholder": "John",
"validateRules": {
"required": true,
"maxLength": 100
},
"index": true
},
{
"label": "Last Name",
"key": "last_name",
"type": "TextField",
"placeholder": "Doe",
"validateRules": {
"required": true,
"maxLength": 100
},
"index": true
},
{
"label": "Email",
"key": "email",
"type": "TextField",
"placeholder": "customer@example.com",
"validateRules": {
"required": true,
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"index": true
},
{
"label": "Phone",
"key": "phone",
"type": "TextField",
"placeholder": "+1 (555) 123-4567"
},
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Active", "Inactive", "Suspended"]
},
"defaultValue": "Active",
"validateRules": {
"required": true
}
},
{
"label": "Customer Type",
"key": "customer_type",
"type": "OptionSet",
"typeOptions": {
"values": ["Retail", "Wholesale", "VIP"]
},
"defaultValue": "Retail"
},
{
"label": "Credit Limit",
"key": "credit_limit",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"defaultValue": 0
},
{
"label": "Total Orders",
"key": "total_orders",
"type": "NumericField",
"typeOptions": {
"decimals": 0
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
},
{
"label": "Lifetime Value",
"key": "lifetime_value",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
}
],
"hooks": []
}
Product Entity
File: src/entities/product.json
{
"head": {
"name": "Product",
"key": "product",
"pluralisedName": "products",
"icon": "ShoppingOutlined",
"allowComments": true,
"allowTasks": false,
"allowAttachments": true,
"isActive": true,
"maxAttachmentSizeAllowed": 5000000,
"acceptedAttachmentFileTypes": ["image/jpeg", "image/png"],
"description": "Product entity for e-commerce module"
},
"fields": [
{
"label": "SKU",
"key": "sku",
"type": "TextField",
"placeholder": "PROD-001",
"description": "Stock Keeping Unit",
"validateRules": {
"required": true,
"maxLength": 50,
"pattern": "^[A-Z0-9-]+$"
},
"index": true
},
{
"label": "Name",
"key": "name",
"type": "TextField",
"placeholder": "Product name",
"validateRules": {
"required": true,
"maxLength": 200
},
"index": true
},
{
"label": "Description",
"key": "description",
"type": "RichTextEditor",
"description": "Detailed product description"
},
{
"label": "Price",
"key": "price",
"type": "NumericField",
"placeholder": "0.00",
"validateRules": {
"required": true,
"min": 0
},
"typeOptions": {
"decimals": 2,
"allowNegative": false
}
},
{
"label": "Cost",
"key": "cost",
"type": "NumericField",
"placeholder": "0.00",
"description": "Product cost (for margin calculation)",
"typeOptions": {
"decimals": 2,
"allowNegative": false
}
},
{
"label": "Stock Quantity",
"key": "stock_quantity",
"type": "NumericField",
"placeholder": "0",
"validateRules": {
"required": true,
"min": 0
},
"typeOptions": {
"decimals": 0,
"allowNegative": false
},
"defaultValue": 0
},
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Active", "Inactive", "Out of Stock"]
},
"defaultValue": "Active",
"validateRules": {
"required": true
}
}
],
"hooks": []
}
Order Entity
File: src/entities/order.json
{
"head": {
"name": "Order",
"key": "order",
"pluralisedName": "orders",
"icon": "ShoppingCartOutlined",
"allowComments": true,
"allowTasks": true,
"isActive": true,
"description": "Order entity for e-commerce module"
},
"fields": [
{
"label": "Order Number",
"key": "order_number",
"type": "TextField",
"placeholder": "ORD-20250111-0001",
"validateRules": {
"required": true
},
"behaviourOptions": {
"readOnly": true
},
"index": true
},
{
"label": "Customer",
"key": "customer_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "customer"
},
"typeOptions": {
"label": "{{first_name}} {{last_name}} ({{email}})"
},
"validateRules": {
"required": true
}
},
{
"label": "Status",
"key": "status",
"type": "OptionSet",
"typeOptions": {
"values": ["Draft", "Pending", "Processing", "Shipped", "Delivered", "Cancelled"]
},
"defaultValue": "Draft",
"validateRules": {
"required": true
}
},
{
"label": "Order Items",
"key": "order_items",
"type": "Grid",
"relationshipOptions": {
"ref": "order_item"
},
"typeOptions": {
"relationshipField": "order_id"
}
},
{
"label": "Subtotal",
"key": "subtotal",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
},
{
"label": "Tax",
"key": "tax_amount",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
},
{
"label": "Total",
"key": "total",
"type": "NumericField",
"typeOptions": {
"decimals": 2,
"allowNegative": false
},
"behaviourOptions": {
"readOnly": true
},
"defaultValue": 0
}
],
"hooks": []
}
Step 4: Creating Reusable Queries
Query Design Principles
- Parameterized - Use parameters for flexibility
- Optimized - Select only necessary fields
- Well-Named - Clear, descriptive names
- Documented - Include descriptions
Customer List Query
File: src/queries/getcustomerlist.json
{
"key": "getcustomerlist",
"name": "getCustomerList",
"description": "Retrieves paginated list of customers with optional filtering",
"connector": "stack9_api",
"queryTemplate": {
"method": "post",
"path": "/customer/search",
"bodyParams": "{\n \"$select\": [\n \"id\",\n \"customer_number\",\n \"first_name\",\n \"last_name\",\n \"email\",\n \"phone\",\n \"status\",\n \"customer_type\",\n \"total_orders\",\n \"lifetime_value\",\n \"_created_at\"\n ],\n \"$where\": {\n \"_is_deleted\": false,\n \"status\": \"{{status}}\",\n \"customer_type\": \"{{customer_type}}\"\n },\n \"$orderBy\": [\n {\"column\": \"{{sortBy}}\", \"order\": \"{{sortOrder}}\"}\n ],\n \"$limit\": {{limit}},\n \"$offset\": {{offset}}\n}",
"queryParams": {}
},
"userParams": {
"status": "",
"customer_type": "",
"sortBy": "_created_at",
"sortOrder": "desc",
"limit": "50",
"offset": "0"
}
}
Order Details Query
File: src/queries/getorderdetails.json
{
"key": "getorderdetails",
"name": "getOrderDetails",
"description": "Retrieves complete order details with customer and items",
"connector": "stack9_api",
"queryTemplate": {
"method": "post",
"path": "/order/search",
"bodyParams": "{\n \"$select\": [\n \"*\",\n \"customer.customer_number\",\n \"customer.first_name\",\n \"customer.last_name\",\n \"customer.email\",\n \"order_items.product.sku\",\n \"order_items.product.name\",\n \"order_items.quantity\",\n \"order_items.unit_price\",\n \"order_items.line_total\"\n ],\n \"$where\": {\n \"id\": {{orderId}}\n },\n \"$withRelated\": [\n \"customer(notDeleted)\",\n \"order_items(notDeleted)\",\n \"order_items(notDeleted).product(notDeleted)\"\n ]\n}",
"queryParams": {}
},
"userParams": {
"orderId": ""
}
}
Product Search Query
File: src/queries/searchproducts.json
{
"key": "searchproducts",
"name": "searchProducts",
"description": "Search products by name or SKU with pagination",
"connector": "stack9_api",
"queryTemplate": {
"method": "post",
"path": "/product/search",
"bodyParams": "{\n \"$select\": [\n \"id\",\n \"sku\",\n \"name\",\n \"price\",\n \"stock_quantity\",\n \"status\"\n ],\n \"$where\": {\n \"_is_deleted\": false,\n \"$or\": [\n {\"name\": {\"$like\": \"%{{searchTerm}}%\"}},\n {\"sku\": {\"$like\": \"%{{searchTerm}}%\"}}\n ],\n \"status\": \"{{status}}\"\n },\n \"$orderBy\": [\n {\"column\": \"name\", \"order\": \"asc\"}\n ],\n \"$limit\": {{limit}},\n \"$offset\": {{offset}}\n}",
"queryParams": {}
},
"userParams": {
"searchTerm": "",
"status": "",
"limit": "20",
"offset": "0"
}
}
Step 5: Creating Reusable Action Types
Action Type Design Principles
- Single Responsibility - Each action does one thing well
- Type-Safe - Use runtypes for validation
- Error Handling - Graceful error handling
- Logging - Comprehensive logging
- Testable - Easy to unit test
Customer Welcome Email Action
File: src/action-types/sendWelcomeEmail.ts
import { Record, String, Number } from 'runtypes';
import { S9AutomationActionType } from '@april9/stack9-sdk';
const SendWelcomeEmailParams = Record({
customer_id: Number,
});
/**
* Send Welcome Email Action
*
* Sends a welcome email to a new customer.
*
* @param customer_id - Customer ID to send email to
* @returns Success status and message ID
*/
export class SendWelcomeEmail extends S9AutomationActionType {
key = 'send_welcome_email';
name = 'Send Welcome Email';
description = 'Sends welcome email to new customer';
async exec(params: any) {
const validated = SendWelcomeEmailParams.check(params);
const { db, connectors, logger } = this.context;
try {
// Fetch customer
const customer = await db('customer')
.where({ id: validated.customer_id })
.first();
if (!customer) {
throw new Error(`Customer ${validated.customer_id} not found`);
}
logger.info(`Sending welcome email to ${customer.email}`);
// Send email via connector
const emailService = connectors['email_service'];
const response = await emailService.call({
method: 'POST',
path: '/send',
body: {
to: customer.email,
template: 'welcome',
data: {
first_name: customer.first_name,
customer_number: customer.customer_number,
},
},
});
logger.info(`Welcome email sent to ${customer.email}`);
return {
success: true,
message_id: response.message_id,
message: 'Welcome email sent successfully',
};
} catch (error) {
logger.error(`Failed to send welcome email: ${error.message}`);
throw error;
}
}
}
Process Order Action
File: src/action-types/processOrder.ts
import { Record, Number } from 'runtypes';
import { S9AutomationActionType } from '@april9/stack9-sdk';
const ProcessOrderParams = Record({
order_id: Number,
});
/**
* Process Order Action
*
* Processes an order: validates stock, updates inventory, and sends confirmation.
*
* @param order_id - Order ID to process
* @returns Processing result with status and details
*/
export class ProcessOrder extends S9AutomationActionType {
key = 'process_order';
name = 'Process Order';
description = 'Process order, update inventory, send confirmation';
async exec(params: any) {
const validated = ProcessOrderParams.check(params);
const { db, logger } = this.context;
try {
// Fetch order with items
const order = await db('order')
.where({ id: validated.order_id })
.first();
if (!order) {
throw new Error(`Order ${validated.order_id} not found`);
}
// Check if already processed
if (order.status !== 'Pending') {
return {
success: false,
error: `Order ${order.order_number} has already been processed`,
};
}
logger.info(`Processing order ${order.order_number}`);
// Get order items
const orderItems = await db('order_item')
.where({ order_id: validated.order_id });
// Validate stock availability
for (const item of orderItems) {
const product = await db('product')
.where({ id: item.product_id })
.first();
if (!product) {
throw new Error(`Product ${item.product_id} not found`);
}
if (product.stock_quantity < item.quantity) {
throw new Error(
`Insufficient stock for ${product.name}. ` +
`Available: ${product.stock_quantity}, Required: ${item.quantity}`
);
}
}
// Update inventory
for (const item of orderItems) {
await db('product')
.where({ id: item.product_id })
.decrement('stock_quantity', item.quantity);
logger.info(`Updated inventory for product ${item.product_id}`);
}
// Update order status
await db('order')
.where({ id: validated.order_id })
.update({
status: 'Processing',
_updated_at: new Date(),
});
logger.info(`Order ${order.order_number} processed successfully`);
return {
success: true,
order_number: order.order_number,
message: 'Order processed successfully',
};
} catch (error) {
logger.error(`Order processing failed: ${error.message}`);
throw error;
}
}
}
Update Customer Metrics Action
File: src/action-types/updateCustomerMetrics.ts
import { Record, Number } from 'runtypes';
import { S9AutomationActionType } from '@april9/stack9-sdk';
const UpdateCustomerMetricsParams = Record({
customer_id: Number,
});
/**
* Update Customer Metrics Action
*
* Recalculates customer metrics: total orders and lifetime value.
*
* @param customer_id - Customer ID to update metrics for
* @returns Updated metrics
*/
export class UpdateCustomerMetrics extends S9AutomationActionType {
key = 'update_customer_metrics';
name = 'Update Customer Metrics';
description = 'Recalculate customer total orders and lifetime value';
async exec(params: any) {
const validated = UpdateCustomerMetricsParams.check(params);
const { db, logger } = this.context;
try {
// Calculate metrics
const metrics = await db('order')
.where({ customer_id: validated.customer_id })
.where({ _is_deleted: false })
.whereIn('status', ['Processing', 'Shipped', 'Delivered'])
.select([
db.raw('COUNT(*) as total_orders'),
db.raw('SUM(total) as lifetime_value'),
])
.first();
const totalOrders = parseInt(metrics.total_orders || '0');
const lifetimeValue = parseFloat(metrics.lifetime_value || '0');
// Update customer record
await db('customer')
.where({ id: validated.customer_id })
.update({
total_orders: totalOrders,
lifetime_value: lifetimeValue,
_updated_at: new Date(),
});
logger.info(
`Updated metrics for customer ${validated.customer_id}: ` +
`${totalOrders} orders, $${lifetimeValue.toFixed(2)} LTV`
);
return {
success: true,
total_orders: totalOrders,
lifetime_value: lifetimeValue,
};
} catch (error) {
logger.error(`Failed to update customer metrics: ${error.message}`);
throw error;
}
}
}
Step 6: Module Entry Point
Create Main Export File
File: src/index.ts
// Import entities
import customerEntity from './entities/customer.json';
import productEntity from './entities/product.json';
import orderEntity from './entities/order.json';
import orderItemEntity from './entities/order_item.json';
// Import queries
import getCustomerListQuery from './queries/getcustomerlist.json';
import getOrderDetailsQuery from './queries/getorderdetails.json';
import searchProductsQuery from './queries/searchproducts.json';
// Import action types
import { SendWelcomeEmail } from './action-types/sendWelcomeEmail';
import { ProcessOrder } from './action-types/processOrder';
import { UpdateCustomerMetrics } from './action-types/updateCustomerMetrics';
// Import entity hooks
import { ValidateCustomer } from './entity-hooks/customer.vat';
import { ValidateOrder } from './entity-hooks/order.vat';
// Import types
export * from './types';
/**
* Stack9 E-Commerce Module
*
* A comprehensive e-commerce solution for Stack9 applications.
* Includes customers, products, orders, and complete business logic.
*
* @version 1.0.0
* @author Your Name
*/
export const EcommerceModule = {
name: '@stack9/ecommerce-module',
version: '1.0.0',
entities: [
customerEntity,
productEntity,
orderEntity,
orderItemEntity,
],
queries: [
getCustomerListQuery,
getOrderDetailsQuery,
searchProductsQuery,
],
actionTypes: [
SendWelcomeEmail,
ProcessOrder,
UpdateCustomerMetrics,
],
hooks: [
ValidateCustomer,
ValidateOrder,
],
connectors: [],
screens: [],
};
export default EcommerceModule;
// Export individual components
export {
// Entities
customerEntity,
productEntity,
orderEntity,
orderItemEntity,
// Queries
getCustomerListQuery,
getOrderDetailsQuery,
searchProductsQuery,
// Action Types
SendWelcomeEmail,
ProcessOrder,
UpdateCustomerMetrics,
// Entity Hooks
ValidateCustomer,
ValidateOrder,
};
Step 7: Module Dependencies
Defining Dependencies
File: package.json (dependencies section)
{
"peerDependencies": {
"@april9/stack9-sdk": "^2.0.0"
},
"dependencies": {
"runtypes": "^6.7.0"
},
"devDependencies": {
"@april9/stack9-sdk": "^2.0.0",
"@types/node": "^18.0.0",
"typescript": "^5.0.0"
}
}
Module Configuration
File: src/module.config.json
{
"name": "@stack9/ecommerce-module",
"version": "1.0.0",
"description": "E-commerce module for Stack9",
"dependencies": {
"required": [],
"optional": [
"@stack9/email-module",
"@stack9/payment-module"
]
},
"configuration": {
"taxRate": {
"type": "number",
"default": 0.1,
"description": "Default tax rate (10%)"
},
"allowNegativeInventory": {
"type": "boolean",
"default": false,
"description": "Allow products to have negative inventory"
},
"orderNumberPrefix": {
"type": "string",
"default": "ORD",
"description": "Prefix for generated order numbers"
}
}
}
Step 8: Module Versioning
Semantic Versioning
Follow semantic versioning (semver):
- Major (X.0.0): Breaking changes
- Minor (0.X.0): New features, backward compatible
- Patch (0.0.X): Bug fixes, backward compatible
Changelog
File: CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2025-01-11
### Added
- Customer entity with lifecycle metrics
- Product entity with inventory management
- Order entity with multi-item support
- Customer list query with filtering
- Order details query with relationships
- Product search query
- Send welcome email action type
- Process order action type
- Update customer metrics action type
- Customer validation hook
- Order validation hook
### Changed
- N/A (initial release)
### Deprecated
- N/A (initial release)
### Removed
- N/A (initial release)
### Fixed
- N/A (initial release)
### Security
- N/A (initial release)
Version Compatibility
File: src/compatibility.ts
/**
* Check if current Stack9 version is compatible with module
*/
export function checkCompatibility(stack9Version: string): boolean {
const [major, minor] = stack9Version.split('.').map(Number);
// This module requires Stack9 v2.x.x or higher
return major >= 2;
}
/**
* Get module version information
*/
export function getModuleInfo() {
return {
name: '@stack9/ecommerce-module',
version: '1.0.0',
compatibleWith: '>=2.0.0',
minStack9Version: '2.0.0',
};
}
Step 9: Packaging and Distribution
Build the Module
# Install dependencies
npm install
# Run linter
npm run lint
# Run tests
npm run test
# Build TypeScript
npm run build
Publish to npm
# Login to npm
npm login
# Publish package
npm publish --access public
# Or publish to private registry
npm publish --registry https://your-private-registry.com
Using the Module
In consuming project:
# Install module
npm install @stack9/ecommerce-module
# Or from private registry
npm install @stack9/ecommerce-module --registry https://your-registry.com
Configure Stack9 to use module:
// src/index.ts in consuming project
import { EcommerceModule } from '@stack9/ecommerce-module';
export const modules = [
EcommerceModule,
];
Alternative: GitHub Distribution
// In consuming project's package.json
{
"dependencies": {
"@stack9/ecommerce-module": "github:yourusername/ecommerce-module#v1.0.0"
}
}
Step 10: Module Documentation
README.md
File: README.md
# Stack9 E-Commerce Module
A comprehensive, reusable e-commerce module for Stack9 applications.
## Features
- **Customer Management**: Track customers, orders, and lifetime value
- **Product Catalog**: Manage products with inventory tracking
- **Order Processing**: Complete order workflow with validation
- **Automated Actions**: Welcome emails, order processing, metrics
- **Business Logic**: Validation hooks and calculated fields
- **Pre-built Queries**: Optimized queries for common operations
## Installation
```bash
npm install @stack9/ecommerce-module
Quick Start
1. Import Module
// src/index.ts
import { EcommerceModule } from '@stack9/ecommerce-module';
export const modules = [
EcommerceModule,
];
2. Configure Module
// src/config/ecommerce.ts
export const ecommerceConfig = {
taxRate: 0.1, // 10% tax
orderNumberPrefix: 'ORD',
allowNegativeInventory: false,
};
3. Use Entities
The module automatically registers these entities:
customer- Customer managementproduct- Product catalogorder- Order processingorder_item- Order line items
4. Use Queries
// Fetch customers
const customers = await api.query.execute('getcustomerlist', {
status: 'Active',
limit: 50,
});
// Get order details
const order = await api.query.execute('getorderdetails', {
orderId: 123,
});
// Search products
const products = await api.query.execute('searchproducts', {
searchTerm: 'laptop',
status: 'Active',
});
5. Use Action Types
// Send welcome email
await api.action.execute('send_welcome_email', {
customer_id: 1,
});
// Process order
await api.action.execute('process_order', {
order_id: 123,
});
// Update customer metrics
await api.action.execute('update_customer_metrics', {
customer_id: 1,
});
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
taxRate | number | 0.1 | Tax rate for orders |
orderNumberPrefix | string | "ORD" | Prefix for order numbers |
allowNegativeInventory | boolean | false | Allow negative stock |
Entities
Customer
Customer entity with the following fields:
customer_number- Unique identifierfirst_name,last_name- Name fieldsemail,phone- Contact informationstatus- Active, Inactive, Suspendedcustomer_type- Retail, Wholesale, VIPtotal_orders- Calculated fieldlifetime_value- Calculated field
Product
Product entity with inventory management:
sku- Stock keeping unitname,description- Product detailsprice,cost- Pricing informationstock_quantity- Current inventorystatus- Active, Inactive, Out of Stock
Order
Order entity with multi-item support:
order_number- Generated identifiercustomer_id- Reference to customerstatus- Draft, Pending, Processing, Shipped, Delivered, Cancelledorder_items- Line itemssubtotal,tax_amount,total- Calculated fields
Queries
getcustomerlist- Paginated customer list with filteringgetorderdetails- Complete order with customer and itemssearchproducts- Search products by name or SKU
Action Types
send_welcome_email- Send welcome email to new customerprocess_order- Process order, update inventoryupdate_customer_metrics- Recalculate customer metrics
License
MIT
Support
- Documentation: https://docs.stack9.com
- GitHub: https://github.com/yourusername/ecommerce-module
- Issues: https://github.com/yourusername/ecommerce-module/issues
### API Documentation
**File:** `docs/api.md`
```markdown
# API Documentation
## Entities
### Customer Entity
**Key:** `customer`
**Fields:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| customer_number | TextField | Yes | Unique customer identifier |
| first_name | TextField | Yes | Customer first name |
| last_name | TextField | Yes | Customer last name |
| email | TextField | Yes | Email address (validated) |
| phone | TextField | No | Phone number |
| status | OptionSet | Yes | Active, Inactive, Suspended |
| customer_type | OptionSet | No | Retail, Wholesale, VIP |
| credit_limit | NumericField | No | Credit limit amount |
| total_orders | NumericField | No | Calculated: total order count |
| lifetime_value | NumericField | No | Calculated: total spent |
**Example:**
```json
{
"customer_number": "CUST-001",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+1 555-123-4567",
"status": "Active",
"customer_type": "Retail",
"credit_limit": 5000.00
}
Queries
getCustomerList
Retrieves paginated list of customers.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| status | string | No | "" | Filter by status |
| customer_type | string | No | "" | Filter by type |
| sortBy | string | No | "_created_at" | Sort field |
| sortOrder | string | No | "desc" | Sort direction |
| limit | number | No | 50 | Results per page |
| offset | number | No | 0 | Results offset |
Example:
const customers = await api.query.execute('getcustomerlist', {
status: 'Active',
customer_type: 'VIP',
sortBy: 'lifetime_value',
sortOrder: 'desc',
limit: 20,
});
Action Types
sendWelcomeEmail
Sends welcome email to new customer.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| customer_id | number | Yes | Customer ID |
Returns:
{
success: boolean;
message_id: string;
message: string;
}
Example:
const result = await api.action.execute('send_welcome_email', {
customer_id: 123,
});
if (result.success) {
console.log('Email sent:', result.message_id);
}
Error Handling:
try {
await api.action.execute('send_welcome_email', { customer_id: 123 });
} catch (error) {
console.error('Failed to send email:', error.message);
}
## Best Practices
### Module Design
1. **Keep It Focused** - Module should solve one problem well
2. **Minimize Dependencies** - Reduce external dependencies
3. **Version Carefully** - Follow semantic versioning strictly
4. **Document Thoroughly** - Clear docs = happy users
5. **Test Extensively** - Write comprehensive tests
### Entity Design
1. **Generic Fields** - Avoid project-specific fields
2. **Sensible Defaults** - Provide good default values
3. **Extensibility** - Allow consumers to extend
4. **Clear Naming** - Use descriptive field names
5. **Proper Indexes** - Index frequently queried fields
### Query Design
1. **Parameterize** - Make queries flexible
2. **Optimize** - Select only needed fields
3. **Document** - Include clear descriptions
4. **Limit Results** - Always paginate
5. **Handle Errors** - Graceful error handling
### Action Type Design
1. **Type Safety** - Use runtypes validation
2. **Error Handling** - Comprehensive error handling
3. **Logging** - Log all important operations
4. **Idempotency** - Safe to run multiple times
5. **Transactions** - Use DB transactions where needed
## Module Testing
### Unit Tests
**File:** `src/action-types/processOrder.test.ts`
```typescript
import { ProcessOrder } from './processOrder';
import { mockContext } from '../test-utils/mockContext';
describe('ProcessOrder', () => {
let action: ProcessOrder;
let context: any;
beforeEach(() => {
context = mockContext();
action = new ProcessOrder(context);
});
it('should process order successfully', async () => {
// Setup test data
const orderId = 1;
context.db.mockOrder(orderId, { status: 'Pending' });
context.db.mockOrderItems(orderId, [
{ product_id: 1, quantity: 2 },
]);
context.db.mockProduct(1, { stock_quantity: 10 });
// Execute
const result = await action.exec({ order_id: orderId });
// Assert
expect(result.success).toBe(true);
expect(result.order_number).toBeDefined();
});
it('should fail with insufficient stock', async () => {
const orderId = 1;
context.db.mockOrder(orderId, { status: 'Pending' });
context.db.mockOrderItems(orderId, [
{ product_id: 1, quantity: 100 },
]);
context.db.mockProduct(1, { stock_quantity: 10 });
await expect(action.exec({ order_id: orderId }))
.rejects.toThrow('Insufficient stock');
});
});
Troubleshooting
Module Not Loading
Problem: Module entities not appearing
Solutions:
- Check module is imported in
src/index.ts - Verify package is installed
- Check Stack9 version compatibility
- Restart Stack9 instance
Query Not Found
Problem: Query not available
Solutions:
- Verify query is exported in module
- Check query key matches
- Ensure module is loaded
- Check Stack9 logs
Action Type Fails
Problem: Action type throws error
Solutions:
- Check parameter validation
- Verify dependencies are installed
- Check database connections
- Review error logs
Next Steps
You've mastered creating reusable modules! Continue learning:
- Performance Optimization - Optimize module performance
- Module Examples - Example modules
Summary
You now understand:
✅ Module structure and organization ✅ Creating reusable entities ✅ Creating reusable queries ✅ Creating reusable action types ✅ Managing module dependencies ✅ Module versioning strategies ✅ Packaging and distribution ✅ Writing comprehensive documentation
Reusable modules accelerate development and promote best practices across teams!