Skip to main content

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:

ParameterTypeRequiredDescription
screenKeystringYesThe key of the screen containing the query (e.g., 'customer_list')
queryNamestringYesThe name of the query to execute (e.g., 'getcustomerlist')
optionsobjectNoQuery execution options
options.varsRecord<string, any>NoVariables to pass to the query (replaces {{paramName}} placeholders)
options.filtersRecord<string, any>NoDynamic filters to apply to the query results
options.sortingRecord<string, any>NoSort configuration for the results
options.querySearchstringNoFull-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:

  1. List Views with Pagination:
const response = await queryService.runNamedQuery(
'product_list',
'getproductlist',
{
vars: { page: 0, limit: 20 },
sorting: { column: 'name', direction: 'asc' }
}
);
  1. Detail Views:
const response = await queryService.runNamedQuery(
'order_detail',
'getorderdetails',
{ vars: { orderId: 456 } }
);
  1. Search and Filter:
const response = await queryService.runNamedQuery(
'customer_list',
'searchcustomers',
{
querySearch: searchTerm,
filters: {
status: { key: 'status', operator: 'equals', value: 'Active' }
}
}
);
  1. Dropdown/Typeahead Options:
const response = await queryService.runNamedQuery(
'product_list',
'getproducts',
{
vars: { page: 0, limit: 10 },
querySearch: userInput
}
);
  1. 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 screenKey must match the key property in your screen definition JSON file
  • The queryName must be defined in the screen's queries array or in the Query Library
  • Query names starting with _ (underscore) are typically auto-generated CRUD queries
  • Empty string values in vars are automatically ignored by Stack9
  • The data property 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 results
  • execute(queryName, params?): Promise<QueryResult> - Execute named query (legacy)
  • executeRaw(sql, params?): Promise<any[]> - Execute raw SQL
  • aggregate(entityKey, options): Promise<AggregateResult> - Execute aggregation
  • getQueryDefinition(queryName): Promise<QueryDefinition> - Get query definition
  • saveQuery(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

  1. Update imports:
// v1.x
import { EntityService } from '@april9au/stack9-sdk';

// v2.x
import { ApiEntityService } from '@april9au/stack9-sdk';
  1. Update service initialization:
// v1.x
const service = new EntityService(apiUrl);

// v2.x
const fetcher = new ApiFetcher(axiosInstance);
const service = new ApiEntityService(fetcher);
  1. 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: