Skip to main content

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

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?

BenefitDescription
ReusabilityUse the same code across multiple projects
ConsistencyStandardize implementations across teams
MaintainabilityUpdate once, benefit everywhere
DistributionShare with community or sell commercially
Faster DevelopmentBootstrap 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

  1. Generic and Flexible - Avoid project-specific fields
  2. Well-Documented - Clear descriptions and examples
  3. Extensible - Allow consumers to extend entities
  4. 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

  1. Parameterized - Use parameters for flexibility
  2. Optimized - Select only necessary fields
  3. Well-Named - Clear, descriptive names
  4. 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

  1. Single Responsibility - Each action does one thing well
  2. Type-Safe - Use runtypes for validation
  3. Error Handling - Graceful error handling
  4. Logging - Comprehensive logging
  5. 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 management
  • product - Product catalog
  • order - Order processing
  • order_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

OptionTypeDefaultDescription
taxRatenumber0.1Tax rate for orders
orderNumberPrefixstring"ORD"Prefix for order numbers
allowNegativeInventorybooleanfalseAllow negative stock

Entities

Customer

Customer entity with the following fields:

  • customer_number - Unique identifier
  • first_name, last_name - Name fields
  • email, phone - Contact information
  • status - Active, Inactive, Suspended
  • customer_type - Retail, Wholesale, VIP
  • total_orders - Calculated field
  • lifetime_value - Calculated field

Product

Product entity with inventory management:

  • sku - Stock keeping unit
  • name, description - Product details
  • price, cost - Pricing information
  • stock_quantity - Current inventory
  • status - Active, Inactive, Out of Stock

Order

Order entity with multi-item support:

  • order_number - Generated identifier
  • customer_id - Reference to customer
  • status - Draft, Pending, Processing, Shipped, Delivered, Cancelled
  • order_items - Line items
  • subtotal, tax_amount, total - Calculated fields

Queries

  • getcustomerlist - Paginated customer list with filtering
  • getorderdetails - Complete order with customer and items
  • searchproducts - Search products by name or SKU

Action Types

  • send_welcome_email - Send welcome email to new customer
  • process_order - Process order, update inventory
  • update_customer_metrics - Recalculate customer metrics

License

MIT

Support


### 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:

ParameterTypeRequiredDefaultDescription
statusstringNo""Filter by status
customer_typestringNo""Filter by type
sortBystringNo"_created_at"Sort field
sortOrderstringNo"desc"Sort direction
limitnumberNo50Results per page
offsetnumberNo0Results 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:

ParameterTypeRequiredDescription
customer_idnumberYesCustomer 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:

  1. Check module is imported in src/index.ts
  2. Verify package is installed
  3. Check Stack9 version compatibility
  4. Restart Stack9 instance

Query Not Found

Problem: Query not available

Solutions:

  1. Verify query is exported in module
  2. Check query key matches
  3. Ensure module is loaded
  4. Check Stack9 logs

Action Type Fails

Problem: Action type throws error

Solutions:

  1. Check parameter validation
  2. Verify dependencies are installed
  3. Check database connections
  4. Review error logs

Next Steps

You've mastered creating reusable modules! Continue learning:

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!