Stack9 SDK Package
The @april9au/stack9-sdk package is the core TypeScript SDK for interacting with Stack9 backend services. It provides type-safe API clients, data models, schema utilities, and query parsing capabilities for building robust Stack9 applications.
Installation
npm install @april9au/stack9-sdk
Package Structure
@april9au/stack9-sdk
├── services/ # API service classes
├── models/ # TypeScript interfaces and data models
├── schema/ # Schema validation and manipulation
├── queryParser/ # Query parsing and building utilities
└── utils/ # Helper functions and utilities
Core Concepts
The SDK follows a service-oriented architecture where each service handles a specific domain of the Stack9 API:
- Services - API client classes for different resources
- Models - TypeScript interfaces defining data structures
- Schema - Runtime schema validation and type generation
- Query Parser - SQL-like query building and parsing
API Services
ApiFetcher
The base HTTP client that all services use for API communication.
import { ApiFetcher } from '@april9au/stack9-sdk';
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: 'https://api.stack9.io',
headers: {
'Content-Type': 'application/json'
}
});
const fetcher = new ApiFetcher(axiosInstance);
Methods:
get<T>(url: string, config?: AxiosRequestConfig): Promise<T>post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>
Interceptors:
// Add request interceptor
fetcher.axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${token}`;
return config;
}
);
// Add response interceptor
fetcher.axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Handle token refresh
}
return Promise.reject(error);
}
);
ApiEntityService
Manages CRUD operations for entities with support for relationships and metadata.
import { ApiEntityService } from '@april9au/stack9-sdk';
const entityService = new ApiEntityService(fetcher);
// Create entity
const customer = await entityService.create('customer', {
name: 'John Doe',
email: 'john@example.com'
});
// Read entity
const customer = await entityService.findById('customer', 123);
// Update entity
const updated = await entityService.update('customer', 123, {
status: 'active'
});
// Delete entity
await entityService.delete('customer', 123);
// List entities with filtering
const customers = await entityService.list('customer', {
filter: { status: 'active' },
sort: 'createdAt DESC',
limit: 20,
offset: 0
});
Core Methods:
create(entityKey: string, data: any): Promise<Entity>findById(entityKey: string, id: number): Promise<Entity>update(entityKey: string, id: number, data: any): Promise<Entity>patch(entityKey: string, id: number, data: any): Promise<Entity>delete(entityKey: string, id: number): Promise<void>list(entityKey: string, options?: ListOptions): Promise<EntityList>
Relationship Methods:
// Get related entities
const orders = await entityService.getRelated(
'customer',
123,
'orders',
{ status: 'completed' }
);
// Associate entities
await entityService.associate(
'customer',
123,
'tags',
[1, 2, 3]
);
// Dissociate entities
await entityService.dissociate(
'customer',
123,
'tags',
[2]
);
Metadata Methods:
// Get entity schema
const schema = await entityService.getSchema('customer');
// Get entity metadata
const metadata = await entityService.getMetadata('customer', 123);
// Update metadata
await entityService.updateMetadata('customer', 123, {
lastContacted: new Date()
});
Bulk Operations:
// Bulk create
const customers = await entityService.bulkCreate('customer', [
{ name: 'Customer 1', email: 'c1@example.com' },
{ name: 'Customer 2', email: 'c2@example.com' }
]);
// Bulk update
await entityService.bulkUpdate('customer',
{ status: 'inactive' },
{ lastLoginDate: { $lt: '2023-01-01' } }
);
// Bulk delete
await entityService.bulkDelete('customer', [1, 2, 3]);
ApiQueryService
Executes custom queries and aggregations. The query service is the primary way to execute screen queries defined in your Stack9 Query Library.
import { ApiQueryService } from '@april9au/stack9-sdk';
const queryService = new ApiQueryService(fetcher);
runNamedQuery()
The most commonly used method for executing queries defined in your Stack9 Query Library. This method executes a named query within the context of a specific screen.
Signature:
runNamedQuery<T>(
screenKey: string,
queryName: string,
options?: {
vars?: Record<string, any>;
filters?: Record<string, any>;
sorting?: Record<string, any>;
querySearch?: string;
}
): Promise<{ data: T }>
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
screenKey | string | Yes | The key of the screen containing the query (e.g., 'customer_list') |
queryName | string | Yes | The name of the query to execute (e.g., 'getcustomerlist') |
options | object | No | Query execution options |
options.vars | Record<string, any> | No | Variables to pass to the query (replaces {{paramName}} placeholders) |
options.filters | Record<string, any> | No | Dynamic filters to apply to the query results |
options.sorting | Record<string, any> | No | Sort configuration for the results |
options.querySearch | string | No | Full-text search term to filter results |
Return Value:
Returns a Promise that resolves to an object with a data property containing the query results. The type of data depends on the query and can be specified using the generic type parameter <T>.
Basic Usage:
// Simple query with no parameters
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist'
);
console.log(response.data); // Array of customers
Using Variables (vars):
Variables are passed to the query and replace {{paramName}} placeholders in the query definition.
// Query a single customer by ID
const response = await queryService.runNamedQuery(
'customer_detail',
'getcustomer',
{
vars: { id: 123 }
}
);
console.log(response.data); // Customer object or array
// Query with pagination variables
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist',
{
vars: {
page: 0,
limit: 20,
status: 'Active'
}
}
);
Using Filters:
Filters are applied dynamically on top of the query's base conditions.
// Apply dynamic filters to results
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist',
{
filters: {
status: {
key: 'status',
operator: 'equals',
value: 'Active'
},
created_at: {
key: 'created_at',
operator: 'greaterThanOrEquals',
value: '2024-01-01'
}
}
}
);
Using Sorting:
Control the order of results dynamically.
// Sort results by multiple columns
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist',
{
sorting: {
column: 'name',
direction: 'asc'
}
}
);
Using Query Search:
Perform full-text search across searchable fields defined in the query.
// Search for customers by name, email, or phone
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist',
{
querySearch: 'john@example.com'
}
);
Complete Example with All Options:
// Full-featured query execution
const response = await queryService.runNamedQuery<Customer[]>(
'customer_list',
'getcustomerlist',
{
vars: {
page: 0,
limit: 50
},
filters: {
status: {
key: 'status',
operator: 'in',
value: ['Active', 'Pending']
}
},
sorting: {
column: '_created_at',
direction: 'desc'
},
querySearch: 'John'
}
);
console.log(response.data); // Typed as Customer[]
Type Safety with Generics:
Specify the expected return type for better TypeScript support:
interface Customer {
id: number;
name: string;
email: string;
status: string;
}
// Single record query
const response = await queryService.runNamedQuery<Customer>(
'customer_detail',
'getcustomer',
{ vars: { id: 123 } }
);
const customer: Customer = response.data;
// Array query
const listResponse = await queryService.runNamedQuery<Customer[]>(
'customer_list',
'getcustomerlist'
);
const customers: Customer[] = listResponse.data;
Common Use Cases:
- List Views with Pagination:
const response = await queryService.runNamedQuery(
'product_list',
'getproductlist',
{
vars: { page: 0, limit: 20 },
sorting: { column: 'name', direction: 'asc' }
}
);
- Detail Views:
const response = await queryService.runNamedQuery(
'order_detail',
'getorderdetails',
{ vars: { orderId: 456 } }
);
- Search and Filter:
const response = await queryService.runNamedQuery(
'customer_list',
'searchcustomers',
{
querySearch: searchTerm,
filters: {
status: { key: 'status', operator: 'equals', value: 'Active' }
}
}
);
- Dropdown/Typeahead Options:
const response = await queryService.runNamedQuery(
'product_list',
'getproducts',
{
vars: { page: 0, limit: 10 },
querySearch: userInput
}
);
- CRUD Operations (with Dynamic Queries):
// Create
await queryService.runNamedQuery(
'customer_screen',
'_customer_create',
{ vars: { name: 'John Doe', email: 'john@example.com' } }
);
// Update
await queryService.runNamedQuery(
'customer_screen',
'_customer_update',
{ vars: { id: 123, name: 'Jane Doe' } }
);
// Delete
await queryService.runNamedQuery(
'customer_screen',
'_customer_delete',
{ vars: { id: 123 } }
);
Error Handling:
try {
const response = await queryService.runNamedQuery(
'customer_list',
'getcustomerlist',
{ vars: { id: 123 } }
);
console.log(response.data);
} catch (error) {
console.error('Query failed:', error.message);
// Handle error (show notification, retry, etc.)
}
Notes:
- The
screenKeymust match thekeyproperty in your screen definition JSON file - The
queryNamemust be defined in the screen'squeriesarray or in the Query Library - Query names starting with
_(underscore) are typically auto-generated CRUD queries - Empty string values in
varsare automatically ignored by Stack9 - The
dataproperty in the response can be an array, object, or null depending on the query
exportScreenQuery()
Exports query results to various formats (CSV, Excel, PDF).
Signature:
exportScreenQuery(
screenKey: string,
queryName: string,
options?: {
vars?: Record<string, any>;
filters?: Record<string, any>;
sorting?: Record<string, any>;
querySearch?: string;
}
): Promise<Blob>
Usage:
// Export customer list to Excel
const blob = await queryService.exportScreenQuery(
'customer_list',
'getcustomerlist',
{
filters: { status: { key: 'status', operator: 'equals', value: 'Active' } }
}
);
// Download the file
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'customers.xlsx';
link.click();
Other Query Methods
Execute Named Query (Legacy):
// Alternative method (older API)
const result = await queryService.execute('monthly-revenue', {
startDate: '2024-01-01',
endDate: '2024-01-31'
});
Execute Raw SQL Query:
const data = await queryService.executeRaw(`
SELECT c.name, COUNT(o.id) as order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id
HAVING order_count > 5
`);
Execute Aggregation:
const stats = await queryService.aggregate('orders', {
groupBy: ['status'],
metrics: [
{ field: 'total', operation: 'sum', alias: 'revenue' },
{ field: 'id', operation: 'count', alias: 'order_count' }
],
having: { revenue: { $gt: 1000 } }
});
Query Builder:
const query = queryService.builder()
.select(['name', 'email', 'status'])
.from('customers')
.where({ status: 'active' })
.andWhere('createdAt', '>=', '2024-01-01')
.orderBy('name', 'ASC')
.limit(10)
.build();
const result = await queryService.executeBuilt(query);
Methods:
runNamedQuery<T>(screenKey, queryName, options?): Promise<{ data: T }>- Execute screen query (recommended)exportScreenQuery(screenKey, queryName, options?): Promise<Blob>- Export query resultsexecute(queryName, params?): Promise<QueryResult>- Execute named query (legacy)executeRaw(sql, params?): Promise<any[]>- Execute raw SQLaggregate(entityKey, options): Promise<AggregateResult>- Execute aggregationgetQueryDefinition(queryName): Promise<QueryDefinition>- Get query definitionsaveQuery(name, definition): Promise<void>- Save query definition
ApiScreenService
Manages screen configurations and routes.
import { ApiScreenService } from '@april9au/stack9-sdk';
const screenService = new ApiScreenService(fetcher);
// Get all screen routes
const routes = await screenService.routes();
// Get screen configuration
const screenConfig = await screenService.getScreen('customer-list');
// Get screen by path
const screen = await screenService.getScreenByPath('/customers');
// Update screen configuration
await screenService.updateScreen('customer-list', {
columnsConfiguration: [
{ field: 'name', title: 'Customer Name', width: 200 }
]
});
// Execute screen query
const data = await screenService.executeScreenQuery(
'customer-list',
{ filter: { status: 'active' } }
);
Methods:
routes(): Promise<ScreenRoute[]>getScreen(screenName: string): Promise<ScreenConfig>getScreenByPath(path: string): Promise<ScreenConfig>updateScreen(screenName: string, config: Partial<ScreenConfig>): Promise<void>createScreen(config: ScreenConfig): Promise<ScreenConfig>deleteScreen(screenName: string): Promise<void>executeScreenQuery(screenName: string, params?: object): Promise<any>
ApiUserService
Manages users, roles, and permissions.
import { ApiUserService } from '@april9au/stack9-sdk';
const userService = new ApiUserService(fetcher);
// Get current user
const currentUser = await userService.getCurrentUser();
// List users
const users = await userService.listUsers({
role: 'admin',
active: true
});
// Create user
const newUser = await userService.createUser({
email: 'user@example.com',
name: 'New User',
role: 'user'
});
// Update user
await userService.updateUser(userId, {
role: 'admin'
});
// User groups
const groups = await userService.listGroups();
await userService.addUserToGroup(userId, groupId);
await userService.removeUserFromGroup(userId, groupId);
Authentication Methods:
// Login
const { token, user } = await userService.login({
email: 'user@example.com',
password: 'password'
});
// Logout
await userService.logout();
// Refresh token
const { token } = await userService.refreshToken(refreshToken);
// Reset password
await userService.requestPasswordReset('user@example.com');
await userService.resetPassword(token, newPassword);
Permission Methods:
// Check permission
const canEdit = await userService.hasPermission(
userId,
'customer:edit'
);
// Get user permissions
const permissions = await userService.getUserPermissions(userId);
// Grant permission
await userService.grantPermission(userId, 'customer:delete');
// Revoke permission
await userService.revokePermission(userId, 'customer:delete');
ApiAppService
Manages application configuration and settings.
import { ApiAppService } from '@april9au/stack9-sdk';
const appService = new ApiAppService(fetcher);
// Get app configuration
const config = await appService.getConfiguration();
// Update configuration
await appService.updateConfiguration({
features: {
darkMode: true,
betaFeatures: false
}
});
// Get app metadata
const metadata = await appService.getMetadata();
// Get feature flags
const features = await appService.getFeatureFlags();
// Update feature flag
await appService.setFeatureFlag('newDashboard', true);
Methods:
getConfiguration(): Promise<AppConfig>updateConfiguration(config: Partial<AppConfig>): Promise<void>getMetadata(): Promise<AppMetadata>getFeatureFlags(): Promise<FeatureFlags>setFeatureFlag(key: string, enabled: boolean): Promise<void>getSettings(namespace: string): Promise<any>updateSettings(namespace: string, settings: any): Promise<void>
ApiAppPermissionService
Manages role-based access control (RBAC).
import { ApiAppPermissionService } from '@april9au/stack9-sdk';
const permissionService = new ApiAppPermissionService(fetcher);
// List roles
const roles = await permissionService.listRoles();
// Create role
const role = await permissionService.createRole({
name: 'Manager',
description: 'Can manage customers and orders',
permissions: ['customer:*', 'order:*']
});
// Update role permissions
await permissionService.updateRolePermissions('manager', [
'customer:read',
'customer:write',
'order:read'
]);
// Check role permission
const canDelete = await permissionService.roleHasPermission(
'manager',
'customer:delete'
);
Methods:
listRoles(): Promise<Role[]>getRole(roleId: string): Promise<Role>createRole(role: RoleDefinition): Promise<Role>updateRole(roleId: string, updates: Partial<Role>): Promise<void>deleteRole(roleId: string): Promise<void>getRolePermissions(roleId: string): Promise<Permission[]>updateRolePermissions(roleId: string, permissions: string[]): Promise<void>
ApiTaskService
Manages tasks and workflows.
import { ApiTaskService } from '@april9au/stack9-sdk';
const taskService = new ApiTaskService(fetcher);
// Create task
const task = await taskService.createTask({
title: 'Follow up with customer',
description: 'Call to discuss renewal',
assignee: userId,
dueDate: new Date('2024-02-01'),
entityKey: 'customer',
entityId: 123
});
// List tasks
const tasks = await taskService.listTasks({
assignee: userId,
status: 'pending',
dueBefore: new Date()
});
// Update task
await taskService.updateTask(taskId, {
status: 'completed',
completedAt: new Date()
});
// Add comment to task
await taskService.addComment(taskId, {
text: 'Customer contacted, will renew next month'
});
Workflow Methods:
// Get workflow definition
const workflow = await taskService.getWorkflow('customer-onboarding');
// Start workflow
const instance = await taskService.startWorkflow(
'customer-onboarding',
{ customerId: 123 }
);
// Get workflow tasks
const tasks = await taskService.getWorkflowTasks(instanceId);
// Complete workflow task
await taskService.completeWorkflowTask(taskId, {
approved: true,
comments: 'Approved by manager'
});
ApiAppAutomationCronJobService
Manages scheduled jobs and automation.
import { ApiAppAutomationCronJobService } from '@april9au/stack9-sdk';
const cronService = new ApiAppAutomationCronJobService(fetcher);
// List cron jobs
const jobs = await cronService.listJobs();
// Create cron job
const job = await cronService.createJob({
name: 'Daily Report',
schedule: '0 9 * * *', // 9 AM daily
action: 'send-daily-report',
enabled: true,
params: {
recipients: ['admin@example.com']
}
});
// Update job
await cronService.updateJob(jobId, {
enabled: false
});
// Execute job manually
await cronService.executeJob(jobId);
// Get job history
const history = await cronService.getJobHistory(jobId);
Data Models
Entity Models
interface Entity {
id: number;
createdAt: Date;
updatedAt: Date;
deletedAt?: Date;
[key: string]: any;
}
interface EntityList {
items: Entity[];
total: number;
offset: number;
limit: number;
hasMore: boolean;
}
interface EntitySchema {
name: string;
tableName: string;
fields: Record<string, FieldDefinition>;
relations: Record<string, RelationDefinition>;
indexes: IndexDefinition[];
validations: ValidationRule[];
}
Field Definitions
interface FieldDefinition {
name: string;
type: FieldType;
required: boolean;
unique: boolean;
default?: any;
description?: string;
validation?: ValidationRule[];
format?: string;
enum?: string[];
min?: number;
max?: number;
precision?: number;
scale?: number;
}
type FieldType =
| 'string'
| 'text'
| 'integer'
| 'decimal'
| 'boolean'
| 'date'
| 'datetime'
| 'time'
| 'json'
| 'uuid'
| 'enum'
| 'array'
| 'file';
Query Models
interface QueryDefinition {
name: string;
description?: string;
sql?: string;
entityKey?: string;
select: string[];
from: string;
joins?: JoinClause[];
where?: WhereClause;
groupBy?: string[];
having?: HavingClause;
orderBy?: OrderByClause[];
limit?: number;
offset?: number;
}
interface QueryResult {
data: any[];
total?: number;
aggregates?: Record<string, any>;
metadata?: Record<string, any>;
}
interface WhereClause {
[field: string]: any | {
$eq?: any;
$ne?: any;
$gt?: any;
$gte?: any;
$lt?: any;
$lte?: any;
$in?: any[];
$nin?: any[];
$like?: string;
$ilike?: string;
$between?: [any, any];
$null?: boolean;
};
}
Screen Models
interface ScreenConfig {
name: string;
head: ScreenHead;
screenType: ScreenType;
listQuery?: string;
detailQuery?: string;
columnsConfiguration?: ColumnConfig[];
components?: ComponentConfig;
formFieldset?: FormFieldset;
widgets?: Widget[];
permissions?: ScreenPermissions;
}
interface ScreenHead {
title: string;
description?: string;
icon?: string;
breadcrumb?: BreadcrumbItem[];
}
type ScreenType =
| 'listView'
| 'detailView'
| 'screenView'
| 'embeddedView'
| 'simpleCrud'
| 'dashboard';
interface ColumnConfig {
field: string;
title: string;
type?: string;
width?: number;
sortable?: boolean;
filterable?: boolean;
render?: string;
format?: string;
hidden?: boolean;
}
User & Permission Models
interface User {
id: number;
email: string;
name: string;
avatar?: string;
role: string;
permissions: string[];
groups: UserGroup[];
metadata?: Record<string, any>;
createdAt: Date;
lastLogin?: Date;
active: boolean;
}
interface Role {
id: string;
name: string;
description?: string;
permissions: Permission[];
inherits?: string[];
createdAt: Date;
updatedAt: Date;
}
interface Permission {
id: string;
resource: string;
action: string;
conditions?: Record<string, any>;
description?: string;
}
interface UserGroup {
id: number;
name: string;
description?: string;
members: User[];
permissions: Permission[];
}
Schema Utilities
Schema Builder
Create and validate schemas programmatically.
import { SchemaBuilder } from '@april9au/stack9-sdk';
const schema = new SchemaBuilder('customer')
.addField('name', 'string', {
required: true,
max: 100
})
.addField('email', 'string', {
required: true,
unique: true,
validation: [{ type: 'email' }]
})
.addField('creditLimit', 'decimal', {
min: 0,
max: 1000000,
precision: 10,
scale: 2
})
.addRelation('orders', {
type: 'hasMany',
target: 'order',
foreign: 'customerId'
})
.addIndex(['email'], { unique: true })
.build();
Schema Validator
Validate data against schemas.
import { SchemaValidator } from '@april9au/stack9-sdk';
const validator = new SchemaValidator(schema);
// Validate data
const result = validator.validate({
name: 'John Doe',
email: 'invalid-email'
});
if (!result.valid) {
console.error('Validation errors:', result.errors);
// [{ field: 'email', message: 'Invalid email format' }]
}
// Validate partial data (for updates)
const updateResult = validator.validatePartial({
creditLimit: 50000
});
Schema Migrator
Generate migration scripts from schema changes.
import { SchemaMigrator } from '@april9au/stack9-sdk';
const migrator = new SchemaMigrator();
// Generate migration
const migration = migrator.generateMigration(
oldSchema,
newSchema
);
console.log(migration.up); // SQL to apply changes
console.log(migration.down); // SQL to revert changes
// Apply migration
await migrator.apply(migration);
// Rollback migration
await migrator.rollback(migration);
Query Parser
Query Builder
Build complex queries with a fluent API.
import { QueryBuilder } from '@april9au/stack9-sdk';
const query = new QueryBuilder()
.select(['c.name', 'c.email', 'COUNT(o.id) as order_count'])
.from('customers', 'c')
.leftJoin('orders', 'o', 'c.id = o.customer_id')
.where('c.status', '=', 'active')
.andWhere('c.createdAt', '>=', '2024-01-01')
.groupBy(['c.id', 'c.name', 'c.email'])
.having('COUNT(o.id)', '>', 5)
.orderBy('order_count', 'DESC')
.limit(10)
.build();
// Use with query service
const result = await queryService.executeRaw(query.toSQL());
Query Parser
Parse Stack9 query syntax.
import { QueryParser } from '@april9au/stack9-sdk';
const parser = new QueryParser();
// Parse Stack9 query syntax
const parsed = parser.parse(`
SELECT name, email
FROM customers
WHERE status = 'active'
AND creditLimit > 1000
ORDER BY name ASC
`);
console.log(parsed);
// {
// select: ['name', 'email'],
// from: 'customers',
// where: {
// status: 'active',
// creditLimit: { $gt: 1000 }
// },
// orderBy: [{ field: 'name', direction: 'ASC' }]
// }
Utility Functions
Data Transformation
import { Utils } from '@april9au/stack9-sdk';
// Transform flat data to nested
const nested = Utils.nest(flatData, 'parent_id');
// Flatten nested data
const flat = Utils.flatten(nestedData);
// Group by field
const grouped = Utils.groupBy(data, 'category');
// Pivot data
const pivoted = Utils.pivot(data, 'month', 'product', 'sales');
Date Utilities
import { DateUtils } from '@april9au/stack9-sdk';
// Parse various date formats
const date = DateUtils.parse('2024-01-15');
// Format dates
const formatted = DateUtils.format(date, 'YYYY-MM-DD');
// Date arithmetic
const tomorrow = DateUtils.addDays(new Date(), 1);
const lastMonth = DateUtils.addMonths(new Date(), -1);
// Date ranges
const range = DateUtils.getDateRange('last_30_days');
const quarters = DateUtils.getQuarters(2024);
Validation Utilities
import { Validators } from '@april9au/stack9-sdk';
// Email validation
const isValidEmail = Validators.email('user@example.com');
// Phone validation
const isValidPhone = Validators.phone('+1-555-123-4567');
// URL validation
const isValidUrl = Validators.url('https://example.com');
// Custom validation
const isValidTaxId = Validators.matches(
'12-3456789',
/^\d{2}-\d{7}$/
);
Encryption Utilities
import { Crypto } from '@april9au/stack9-sdk';
// Hash passwords
const hash = await Crypto.hash('password123');
const isValid = await Crypto.verify('password123', hash);
// Encrypt/decrypt data
const encrypted = await Crypto.encrypt('sensitive data', key);
const decrypted = await Crypto.decrypt(encrypted, key);
// Generate tokens
const token = Crypto.generateToken(32);
const uuid = Crypto.generateUUID();
Advanced Usage
Custom Service Extension
Extend services with custom functionality.
class CustomEntityService extends ApiEntityService {
async findByEmail(entityKey: string, email: string) {
const result = await this.list(entityKey, {
filter: { email },
limit: 1
});
return result.items[0];
}
async softDelete(entityKey: string, id: number) {
return this.patch(entityKey, id, {
deletedAt: new Date(),
active: false
});
}
}
Request Interceptors
Add custom logic to all API requests.
class AuthInterceptor {
constructor(private tokenProvider: () => string) {}
intercept(config: AxiosRequestConfig) {
const token = this.tokenProvider();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}
}
// Apply interceptor
fetcher.axios.interceptors.request.use(
config => new AuthInterceptor(getToken).intercept(config)
);
Caching Layer
Implement caching for better performance.
class CachedEntityService extends ApiEntityService {
private cache = new Map();
async findById(entityKey: string, id: number) {
const cacheKey = `${entityKey}:${id}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const entity = await super.findById(entityKey, id);
this.cache.set(cacheKey, entity);
// Clear cache after 5 minutes
setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000);
return entity;
}
}
Batch Operations
Optimize multiple operations with batching.
class BatchProcessor {
private queue: Array<() => Promise<any>> = [];
add(operation: () => Promise<any>) {
this.queue.push(operation);
}
async execute(concurrency = 5) {
const results = [];
for (let i = 0; i < this.queue.length; i += concurrency) {
const batch = this.queue.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map(op => op().catch(e => ({ error: e })))
);
results.push(...batchResults);
}
return results;
}
}
// Usage
const processor = new BatchProcessor();
customerIds.forEach(id => {
processor.add(() => entityService.findById('customer', id));
});
const customers = await processor.execute();
Error Handling
Error Types
import {
ApiError,
ValidationError,
NotFoundError,
UnauthorizedError,
ForbiddenError
} from '@april9au/stack9-sdk';
try {
await entityService.findById('customer', 999);
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Customer not found');
} else if (error instanceof ValidationError) {
console.log('Validation errors:', error.errors);
} else if (error instanceof UnauthorizedError) {
// Redirect to login
} else if (error instanceof ForbiddenError) {
console.log('Access denied');
} else if (error instanceof ApiError) {
console.log('API error:', error.message, error.statusCode);
}
}
Error Recovery
class ResilientApiService {
async executeWithRetry<T>(
operation: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;
// Don't retry on client errors
if (error.statusCode >= 400 && error.statusCode < 500) {
throw error;
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
throw lastError;
}
}
Testing
Mock Services
import { MockEntityService } from '@april9au/stack9-sdk/testing';
describe('CustomerService', () => {
let service: MockEntityService;
beforeEach(() => {
service = new MockEntityService();
service.addMockData('customer', [
{ id: 1, name: 'Test Customer' }
]);
});
test('finds customer by id', async () => {
const customer = await service.findById('customer', 1);
expect(customer.name).toBe('Test Customer');
});
});
Test Utilities
import { TestDataBuilder } from '@april9au/stack9-sdk/testing';
const builder = new TestDataBuilder();
// Generate test data
const customers = builder.generate('customer', 10, {
name: () => faker.name.fullName(),
email: () => faker.internet.email(),
creditLimit: () => faker.number.int({ min: 1000, max: 10000 })
});
// Seed database
await builder.seed(entityService, {
customers: 10,
orders: 50,
products: 20
});
Best Practices
1. Service Initialization
Initialize services once and reuse:
// services/index.ts
import { ApiFetcher, ApiEntityService, ApiQueryService } from '@april9au/stack9-sdk';
import { axiosInstance } from './axios';
const fetcher = new ApiFetcher(axiosInstance);
export const entityService = new ApiEntityService(fetcher);
export const queryService = new ApiQueryService(fetcher);
// Use throughout app
import { entityService } from '@/services';
2. Type Safety
Always use TypeScript interfaces:
interface Customer extends Entity {
name: string;
email: string;
creditLimit: number;
status: 'active' | 'inactive';
}
// Type-safe service wrapper
class CustomerService {
async findById(id: number): Promise<Customer> {
return entityService.findById('customer', id) as Promise<Customer>;
}
async list(options?: ListOptions): Promise<EntityList<Customer>> {
return entityService.list('customer', options) as Promise<EntityList<Customer>>;
}
}
3. Error Boundaries
Implement comprehensive error handling:
class ApiErrorBoundary {
static async handle<T>(
operation: () => Promise<T>,
fallback?: T
): Promise<T> {
try {
return await operation();
} catch (error) {
// Log to monitoring service
console.error('API Error:', error);
// Show user-friendly message
if (error instanceof ApiError) {
notification.error({
message: 'Operation failed',
description: error.userMessage || error.message
});
}
if (fallback !== undefined) {
return fallback;
}
throw error;
}
}
}
4. Optimistic Updates
Improve perceived performance:
class OptimisticEntityService {
async update(entityKey: string, id: number, data: any) {
// Update UI immediately
updateLocalState(id, data);
try {
const result = await entityService.update(entityKey, id, data);
// Confirm with server data
updateLocalState(id, result);
return result;
} catch (error) {
// Revert on failure
revertLocalState(id);
throw error;
}
}
}
Migration Guide
From v1.x to v2.x
- Update imports:
// v1.x
import { EntityService } from '@april9au/stack9-sdk';
// v2.x
import { ApiEntityService } from '@april9au/stack9-sdk';
- Update service initialization:
// v1.x
const service = new EntityService(apiUrl);
// v2.x
const fetcher = new ApiFetcher(axiosInstance);
const service = new ApiEntityService(fetcher);
- Update method calls:
// v1.x
service.get('customer', 123);
// v2.x
service.findById('customer', 123);
Performance Tips
1. Use Pagination
Always paginate large datasets:
const pageSize = 50;
let offset = 0;
let hasMore = true;
while (hasMore) {
const result = await entityService.list('customer', {
limit: pageSize,
offset
});
processCustomers(result.items);
offset += pageSize;
hasMore = result.hasMore;
}
2. Select Only Required Fields
const customers = await queryService.execute('customer-list', {
select: ['id', 'name', 'email'] // Don't fetch unnecessary fields
});
3. Use Proper Indexing
// Add indexes for frequently queried fields
const schema = new SchemaBuilder('customer')
.addIndex(['email'], { unique: true })
.addIndex(['status', 'createdAt'])
.build();
4. Implement Caching
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({
max: 500,
ttl: 1000 * 60 * 5 // 5 minutes
});
async function getCachedEntity(key: string, id: number) {
const cacheKey = `${key}:${id}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const entity = await entityService.findById(key, id);
cache.set(cacheKey, entity);
return entity;
}
Troubleshooting
Common Issues
Issue: 401 Unauthorized errors
- Solution: Check token expiration and refresh mechanism
Issue: Timeout on large queries
- Solution: Use pagination or optimize query with proper indexes
Issue: CORS errors in browser
- Solution: Configure CORS properly on Stack9 backend
Issue: Type errors with TypeScript
- Solution: Ensure @types/stack9-sdk is installed and up to date
Support
For issues, feature requests, or questions:
- GitHub Issues: stack9-monorepo/issues
- Documentation: stack9.docs
- Community: Stack9 Discord