@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
2,008 lines (1,759 loc) • 60.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.actionheroTemplate = void 0;
exports.actionheroTemplate = {
id: 'actionherojs',
name: 'actionherojs',
displayName: 'ActionHero',
description: 'Multi-transport API server with clustering, real-time capabilities, and background jobs',
language: 'javascript',
framework: 'actionhero',
version: '29.0.0',
tags: ['multi-transport', 'clustering', 'real-time', 'background-jobs', 'api'],
port: 8080,
dependencies: {},
features: ['websockets', 'clustering', 'background-jobs', 'real-time', 'multi-transport'],
packageJson: {
name: 'actionhero-backend',
version: '1.0.0',
description: 'ActionHero multi-transport API server',
scripts: {
start: 'actionhero start',
'start:cluster': 'actionhero start cluster --workers=4',
dev: 'actionhero start --watch',
test: 'jest',
'test:watch': 'jest --watch',
'test:coverage': 'jest --coverage',
actionhero: 'actionhero',
build: 'tsc',
'docker:build': 'docker build -t actionhero-backend .',
'docker:run': 'docker run -p 8080:8080 -p 5000:5000 actionhero-backend',
'generate:action': 'actionhero generate action',
'generate:task': 'actionhero generate task',
'generate:initializer': 'actionhero generate initializer',
'generate:server': 'actionhero generate server'
},
dependencies: {
actionhero: '^29.0.0',
redis: '^4.6.0',
ioredis: '^5.3.0',
winston: '^3.11.0',
'winston-daily-rotate-file': '^4.7.1',
bcrypt: '^5.1.0',
jsonwebtoken: '^9.0.0',
uuid: '^9.0.0',
axios: '^1.6.0',
'node-schedule': '^2.1.0',
pg: '^8.11.0',
sequelize: '^6.35.0',
'sequelize-typescript': '^2.1.6'
},
devDependencies: {
'@types/node': '^20.10.0',
'@types/jest': '^29.5.0',
'@types/bcrypt': '^5.0.0',
'@types/jsonwebtoken': '^9.0.0',
'@types/uuid': '^9.0.0',
'@types/node-schedule': '^2.1.0',
typescript: '^5.3.0',
jest: '^29.7.0',
'ts-jest': '^29.1.0',
'ts-node': '^10.9.0',
nodemon: '^3.0.0',
supertest: '^6.3.0'
},
engines: {
node: '>=18.0.0'
}
},
files: [
{
path: 'config/servers/web.ts',
content: `export const DEFAULT = {
servers: {
web: (config: any) => {
return {
enabled: true,
secure: false,
serverOptions: {},
allowedRequestHosts: process.env.ALLOWED_HOSTS?.split(',') || [],
port: process.env.WEB_PORT || 8080,
bindIP: '0.0.0.0',
httpHeaders: {
'X-Powered-By': config.general.serverName,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
},
urlPathForActions: 'api',
urlPathForFiles: 'public',
rootEndpointType: 'file',
directoryFileType: 'index.html',
fingerprintOptions: {
fingerprintOnlyStaticFiles: false
},
formOptions: {
uploadDir: '/tmp',
keepExtensions: false,
maxFieldsSize: 1024 * 1024 * 20,
maxFileSize: 1024 * 1024 * 200
},
metadataOptions: {
serverInformation: true,
requesterInformation: true
},
returnErrorCodes: true,
compress: true,
padding: 2,
metadataOptions: {
serverInformation: true,
requesterInformation: true
}
};
}
}
};
export const test = {
servers: {
web: (config: any) => {
return {
enabled: true,
secure: false,
port: 18080 + parseInt(process.env.JEST_WORKER_ID || '0'),
matchExtensionMime: true,
metadataOptions: {
serverInformation: true,
requesterInformation: true
}
};
}
}
};`
},
{
path: 'config/servers/websocket.ts',
content: `export const DEFAULT = {
servers: {
websocket: (config: any) => {
return {
enabled: true,
clientUrl: 'http://localhost:8080',
clientJsPath: 'public/javascript/',
clientJsName: 'ActionheroWebsocketClient.min.js',
clientApiPath: 'public/api/',
headers: {},
logLevel: 'info',
verbs: [
'quit',
'exit',
'roomAdd',
'roomLeave',
'roomView',
'detailsView',
'say',
'file',
'event',
'documentation',
'status',
'time'
]
};
}
}
};
export const test = {
servers: {
websocket: (config: any) => {
return {
enabled: true,
port: 19000 + parseInt(process.env.JEST_WORKER_ID || '0'),
clientUrl: 'http://localhost:8080'
};
}
}
};`
},
{
path: 'config/servers/socket.ts',
content: `export const DEFAULT = {
servers: {
socket: (config: any) => {
return {
enabled: true,
secure: false,
serverOptions: {},
port: process.env.SOCKET_PORT || 5000,
bindIP: '0.0.0.0',
setKeepAlive: false,
maxConnections: 0,
logLevel: 'info'
};
}
}
};
export const test = {
servers: {
socket: (config: any) => {
return {
enabled: true,
port: 15000 + parseInt(process.env.JEST_WORKER_ID || '0')
};
}
}
};`
},
{
path: 'config/redis.ts',
content: `import { URL } from 'url';
let host = process.env.REDIS_HOST || '127.0.0.1';
let port = process.env.REDIS_PORT || 6379;
let db = process.env.REDIS_DB || 0;
let password = process.env.REDIS_PASSWORD || null;
if (process.env.REDIS_URL) {
const parsed = new URL(process.env.REDIS_URL);
host = parsed.hostname;
port = parsed.port || 6379;
password = parsed.password;
if (parsed.pathname) {
db = parseInt(parsed.pathname.substring(1));
}
}
export const DEFAULT = {
redis: (config: any) => {
const redisConfig: any = {
enabled: true,
_toExpand: false,
client: {
host,
port,
password,
db,
buildNew: true
},
subscriber: {
host,
port,
password,
db,
buildNew: true
},
tasks: {
host,
port,
password,
db,
buildNew: true
}
};
return redisConfig;
}
};
export const test = {
redis: (config: any) => {
const testDb = 10 + parseInt(process.env.JEST_WORKER_ID || '0');
return {
enabled: true,
_toExpand: false,
client: {
host: '127.0.0.1',
port: 6379,
password: null,
db: testDb,
buildNew: true
},
subscriber: {
host: '127.0.0.1',
port: 6379,
password: null,
db: testDb,
buildNew: true
},
tasks: {
host: '127.0.0.1',
port: 6379,
password: null,
db: testDb,
buildNew: true
}
};
}
};`
},
{
path: 'config/tasks.ts',
content: `export const DEFAULT = {
tasks: (config: any) => {
return {
scheduler: true,
queues: ['*'],
verbose: true,
workerLogging: {
failure: 'error',
success: 'info',
start: 'info',
end: 'info',
cleaning_worker: 'info',
poll: 'debug',
job: 'debug',
pause: 'debug',
internalError: 'error',
multiWorkerAction: 'debug'
},
stuckWorkerTimeout: 1000 * 60 * 60,
checkTimeout: 500,
maxEventLoopDelay: 5,
toDisconnectProcessors: true,
redis: config.redis
};
}
};
export const test = {
tasks: (config: any) => {
return {
scheduler: false,
queues: ['*'],
checkTimeout: 100,
maxEventLoopDelay: 5
};
}
};`
},
{
path: 'config/routes.ts',
content: `export const DEFAULT = {
routes: (config: any) => {
return {
// Basic REST routes
get: [
{ path: '/status', action: 'status' },
{ path: '/swagger', action: 'swagger' },
{ path: '/users', action: 'users:list' },
{ path: '/users/:id', action: 'users:show' },
{ path: '/products', action: 'products:list' },
{ path: '/products/:id', action: 'products:show' },
{ path: '/health', action: 'health' }
],
post: [
{ path: '/auth/login', action: 'auth:login' },
{ path: '/auth/register', action: 'auth:register' },
{ path: '/auth/refresh', action: 'auth:refresh' },
{ path: '/users', action: 'users:create' },
{ path: '/products', action: 'products:create' },
{ path: '/jobs/:name', action: 'jobs:enqueue' },
{ path: '/chat/send', action: 'chat:send' }
],
put: [
{ path: '/users/:id', action: 'users:update' },
{ path: '/products/:id', action: 'products:update' }
],
patch: [
{ path: '/users/:id', action: 'users:patch' },
{ path: '/products/:id', action: 'products:patch' }
],
delete: [
{ path: '/users/:id', action: 'users:delete' },
{ path: '/products/:id', action: 'products:delete' },
{ path: '/cache/:key', action: 'cache:delete' }
],
// Versioned routes
all: [
{ path: '/v1/:action', action: 'v1:%action' },
{ path: '/v2/:action', action: 'v2:%action' }
]
};
}
};`
},
{
path: 'actions/status.ts',
content: `import { Action, api } from 'actionhero';
export class StatusAction extends Action {
constructor() {
super();
this.name = 'status';
this.description = 'Get server status and statistics';
this.outputExample = {
id: '192.168.2.11',
actionheroVersion: '29.0.0',
uptime: 10469,
stats: {
connections: 0,
actions: 5,
tasks: 0
}
};
}
async run({ response }: { response: any }) {
const stats = await api.resque.queue.stats();
response.id = api.id;
response.actionheroVersion = api.actionheroVersion;
response.uptime = process.uptime();
response.nodeVersion = process.version;
response.pid = process.pid;
response.stats = {
connections: Object.keys(api.connections.connections).length,
actions: Object.keys(api.actions.actions).length,
tasks: Object.keys(api.tasks.tasks).length,
queue: stats
};
response.serverInformation = {
serverName: api.config.general.serverName,
apiVersion: api.config.general.apiVersion,
requestDuration: new Date().getTime() - response.messageId,
currentTime: new Date().getTime()
};
response.health = 'OK';
}
}
`
},
{
path: 'actions/users.ts',
content: `import { Action, api, ParamsFrom } from 'actionhero';
import { User } from '../models/User';
import bcrypt from 'bcrypt';
import { Op } from 'sequelize';
// List users action
export class UsersList extends Action {
constructor() {
super();
this.name = 'users:list';
this.description = 'List all users with pagination';
this.inputs = {
page: { required: false, default: 1 },
limit: { required: false, default: 20 },
search: { required: false }
};
this.middleware = ['auth'];
this.outputExample = {
users: [],
pagination: {
page: 1,
limit: 20,
total: 100
}
};
}
async run(data: ParamsFrom<UsersList>) {
const { page = 1, limit = 20, search } = data.params;
const offset = (page - 1) * limit;
const where: any = {};
if (search) {
where[Op.or] = [
{ name: { [Op.iLike]: \`%\${search}%\` } },
{ email: { [Op.iLike]: \`%\${search}%\` } }
];
}
const { rows: users, count } = await User.findAndCountAll({
where,
limit,
offset,
attributes: { exclude: ['password'] },
order: [['createdAt', 'DESC']]
});
data.response.users = users;
data.response.pagination = {
page,
limit,
total: count,
totalPages: Math.ceil(count / limit)
};
}
}
// Get single user action
export class UsersShow extends Action {
constructor() {
super();
this.name = 'users:show';
this.description = 'Get a single user by ID';
this.inputs = {
id: { required: true }
};
this.middleware = ['auth'];
}
async run(data: ParamsFrom<UsersShow>) {
const user = await User.findByPk(data.params.id, {
attributes: { exclude: ['password'] }
});
if (!user) {
throw new Error('User not found');
}
data.response.user = user;
}
}
// Create user action
export class UsersCreate extends Action {
constructor() {
super();
this.name = 'users:create';
this.description = 'Create a new user';
this.inputs = {
name: { required: true },
email: { required: true, validator: 'email' },
password: { required: true, validator: 'min:6' },
role: { required: false, default: 'user' }
};
this.middleware = ['auth', 'adminOnly'];
}
async run(data: ParamsFrom<UsersCreate>) {
const { name, email, password, role } = data.params;
// Check if user exists
const existing = await User.findOne({ where: { email } });
if (existing) {
throw new Error('User already exists with this email');
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await User.create({
name,
email,
password: hashedPassword,
role
});
// Remove password from response
const userData = user.toJSON();
delete userData.password;
data.response.user = userData;
}
}
// Update user action
export class UsersUpdate extends Action {
constructor() {
super();
this.name = 'users:update';
this.description = 'Update a user';
this.inputs = {
id: { required: true },
name: { required: false },
email: { required: false, validator: 'email' },
password: { required: false, validator: 'min:6' },
role: { required: false }
};
this.middleware = ['auth', 'ownerOrAdmin'];
}
async run(data: ParamsFrom<UsersUpdate>) {
const { id, ...updates } = data.params;
const user = await User.findByPk(id);
if (!user) {
throw new Error('User not found');
}
// Hash password if provided
if (updates.password) {
updates.password = await bcrypt.hash(updates.password, 10);
}
// Update user
await user.update(updates);
// Remove password from response
const userData = user.toJSON();
delete userData.password;
data.response.user = userData;
}
}
// Delete user action
export class UsersDelete extends Action {
constructor() {
super();
this.name = 'users:delete';
this.description = 'Delete a user';
this.inputs = {
id: { required: true }
};
this.middleware = ['auth', 'adminOnly'];
}
async run(data: ParamsFrom<UsersDelete>) {
const user = await User.findByPk(data.params.id);
if (!user) {
throw new Error('User not found');
}
await user.destroy();
data.response.success = true;
}
}
`
},
{
path: 'actions/auth.ts',
content: `import { Action, api, ParamsFrom } from 'actionhero';
import { User } from '../models/User';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
export class AuthLogin extends Action {
constructor() {
super();
this.name = 'auth:login';
this.description = 'Authenticate user and return JWT token';
this.inputs = {
email: { required: true, validator: 'email' },
password: { required: true }
};
this.outputExample = {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
user: {
id: 1,
name: 'John Doe',
email: 'john@example.com'
}
};
}
async run(data: ParamsFrom<AuthLogin>) {
const { email, password } = data.params;
// Find user
const user = await User.findOne({ where: { email } });
if (!user) {
throw new Error('Invalid credentials');
}
// Verify password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
throw new Error('Invalid credentials');
}
// Generate token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
// Cache token for validation
await api.cache.save(\`auth:token:\${user.id}\`, token, 1000 * 60 * 60 * 24 * 7);
// Return token and user info
const userData = user.toJSON();
delete userData.password;
data.response.token = token;
data.response.user = userData;
}
}
export class AuthRegister extends Action {
constructor() {
super();
this.name = 'auth:register';
this.description = 'Register a new user';
this.inputs = {
name: { required: true },
email: { required: true, validator: 'email' },
password: { required: true, validator: 'min:6' }
};
}
async run(data: ParamsFrom<AuthRegister>) {
const { name, email, password } = data.params;
// Check if user exists
const existing = await User.findOne({ where: { email } });
if (existing) {
throw new Error('User already exists with this email');
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await User.create({
name,
email,
password: hashedPassword,
role: 'user'
});
// Generate token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
// Cache token
await api.cache.save(\`auth:token:\${user.id}\`, token, 1000 * 60 * 60 * 24 * 7);
// Return token and user info
const userData = user.toJSON();
delete userData.password;
data.response.token = token;
data.response.user = userData;
}
}
export class AuthRefresh extends Action {
constructor() {
super();
this.name = 'auth:refresh';
this.description = 'Refresh JWT token';
this.middleware = ['auth'];
}
async run(data: ParamsFrom<AuthRefresh>) {
const { user } = data.session;
// Generate new token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
// Update cached token
await api.cache.save(\`auth:token:\${user.id}\`, token, 1000 * 60 * 60 * 24 * 7);
data.response.token = token;
}
}
`
},
{
path: 'actions/chat.ts',
content: `import { Action, api, ParamsFrom, chatRoom } from 'actionhero';
export class ChatSend extends Action {
constructor() {
super();
this.name = 'chat:send';
this.description = 'Send a message to a chat room';
this.inputs = {
room: { required: true },
message: { required: true }
};
this.middleware = ['auth'];
}
async run(data: ParamsFrom<ChatSend>) {
const { room, message } = data.params;
const { user } = data.session;
// Check if user is in the room
const rooms = await api.chatRoom.roomStatus(room);
const userInRoom = rooms.members.includes(data.connection.id);
if (!userInRoom) {
throw new Error('You must join the room first');
}
// Broadcast message to room
await api.chatRoom.broadcast(
{},
room,
{
type: 'message',
from: user.name,
userId: user.id,
message,
timestamp: new Date().toISOString()
}
);
// Save message to database (optional)
await api.tasks.enqueue('saveMessage', {
room,
userId: user.id,
message,
timestamp: new Date()
}, 'chat');
data.response.success = true;
}
}
export class ChatJoin extends Action {
constructor() {
super();
this.name = 'chat:join';
this.description = 'Join a chat room';
this.inputs = {
room: { required: true }
};
this.middleware = ['auth'];
}
async run(data: ParamsFrom<ChatJoin>) {
const { room } = data.params;
const { user } = data.session;
// Add connection to room
await api.chatRoom.addMember(data.connection.id, room);
// Broadcast join message
await api.chatRoom.broadcast(
{ id: data.connection.id },
room,
{
type: 'userJoined',
user: user.name,
userId: user.id,
timestamp: new Date().toISOString()
}
);
data.response.success = true;
data.response.room = room;
}
}
export class ChatLeave extends Action {
constructor() {
super();
this.name = 'chat:leave';
this.description = 'Leave a chat room';
this.inputs = {
room: { required: true }
};
this.middleware = ['auth'];
}
async run(data: ParamsFrom<ChatLeave>) {
const { room } = data.params;
const { user } = data.session;
// Remove connection from room
await api.chatRoom.removeMember(data.connection.id, room);
// Broadcast leave message
await api.chatRoom.broadcast(
{},
room,
{
type: 'userLeft',
user: user.name,
userId: user.id,
timestamp: new Date().toISOString()
}
);
data.response.success = true;
}
}
export class ChatRooms extends Action {
constructor() {
super();
this.name = 'chat:rooms';
this.description = 'List available chat rooms';
this.middleware = ['auth'];
}
async run(data: ParamsFrom<ChatRooms>) {
const rooms = await api.chatRoom.availableRooms();
const roomsWithInfo = [];
for (const room of rooms) {
const status = await api.chatRoom.roomStatus(room);
roomsWithInfo.push({
name: room,
memberCount: status.members.length,
members: status.members
});
}
data.response.rooms = roomsWithInfo;
}
}
`
},
{
path: 'actions/jobs.ts',
content: `import { Action, api, ParamsFrom } from 'actionhero';
export class JobsEnqueue extends Action {
constructor() {
super();
this.name = 'jobs:enqueue';
this.description = 'Enqueue a background job';
this.inputs = {
name: { required: true },
args: { required: false, default: {} },
queue: { required: false, default: 'default' },
delay: { required: false, default: 0 }
};
this.middleware = ['auth'];
}
async run(data: ParamsFrom<JobsEnqueue>) {
const { name, args, queue, delay } = data.params;
// Check if task exists
if (!api.tasks.tasks[name]) {
throw new Error(\`Task '\${name}' does not exist\`);
}
// Enqueue the job
if (delay > 0) {
await api.tasks.enqueueIn(delay, name, args, queue);
} else {
await api.tasks.enqueue(name, args, queue);
}
data.response.success = true;
data.response.job = {
name,
args,
queue,
delay,
enqueuedAt: new Date().toISOString()
};
}
}
export class JobsStatus extends Action {
constructor() {
super();
this.name = 'jobs:status';
this.description = 'Get job queue status';
this.middleware = ['auth', 'adminOnly'];
}
async run(data: ParamsFrom<JobsStatus>) {
const stats = await api.resque.queue.stats();
const workers = await api.resque.queue.workers();
const failed = await api.resque.queue.failed();
const queues = await api.resque.queue.queues();
const queueLengths: any = {};
for (const queue of queues) {
queueLengths[queue] = await api.resque.queue.length(queue);
}
data.response.stats = {
...stats,
workers: workers.length,
failed: failed,
queues: queueLengths
};
}
}
export class JobsRetry extends Action {
constructor() {
super();
this.name = 'jobs:retry';
this.description = 'Retry failed jobs';
this.inputs = {
start: { required: false, default: 0 },
stop: { required: false, default: -1 }
};
this.middleware = ['auth', 'adminOnly'];
}
async run(data: ParamsFrom<JobsRetry>) {
const { start, stop } = data.params;
const failed = await api.resque.queue.failed();
const endIndex = stop === -1 ? failed : Math.min(stop, failed);
let retried = 0;
for (let i = start; i < endIndex; i++) {
try {
await api.resque.queue.retryAndRemoveFailed(i);
retried++;
} catch (error) {
api.log(\`Failed to retry job at index \${i}: \${error.message}\`, 'error');
}
}
data.response.retried = retried;
data.response.success = true;
}
}
`
},
{
path: 'tasks/email.ts',
content: `import { Task, api, log, config } from 'actionhero';
export class SendEmail extends Task {
constructor() {
super();
this.name = 'sendEmail';
this.description = 'Send email notifications';
this.frequency = 0; // No automatic runs
this.queue = 'email';
this.middleware = [];
}
async run(params: {
to: string;
subject: string;
body: string;
template?: string;
data?: any;
}) {
const { to, subject, body, template, data } = params;
api.log(\`Sending email to \${to}: \${subject}\`, 'info');
try {
// Here you would integrate with your email service
// Example: SendGrid, AWS SES, Mailgun, etc.
// For demo purposes, we'll just log it
api.log(\`Email sent successfully to \${to}\`, 'info');
// Track email sent
await api.cache.increment('stats:emails:sent');
return { success: true, to, subject };
} catch (error) {
api.log(\`Failed to send email to \${to}: \${error.message}\`, 'error');
// Track failed email
await api.cache.increment('stats:emails:failed');
// Re-throw to mark task as failed
throw error;
}
}
}
export class SendBulkEmail extends Task {
constructor() {
super();
this.name = 'sendBulkEmail';
this.description = 'Send bulk email to multiple recipients';
this.frequency = 0;
this.queue = 'email';
}
async run(params: {
recipients: string[];
subject: string;
body: string;
template?: string;
batchSize?: number;
}) {
const { recipients, subject, body, template, batchSize = 50 } = params;
api.log(\`Sending bulk email to \${recipients.length} recipients\`, 'info');
// Process in batches
for (let i = 0; i < recipients.length; i += batchSize) {
const batch = recipients.slice(i, i + batchSize);
// Queue individual emails
for (const recipient of batch) {
await api.tasks.enqueue('sendEmail', {
to: recipient,
subject,
body,
template
}, 'email');
}
// Small delay between batches
await new Promise(resolve => setTimeout(resolve, 1000));
}
return {
success: true,
totalRecipients: recipients.length,
batches: Math.ceil(recipients.length / batchSize)
};
}
}
`
},
{
path: 'tasks/cleanup.ts',
content: `import { Task, api } from 'actionhero';
import { Op } from 'sequelize';
import { Session } from '../models/Session';
import { AuditLog } from '../models/AuditLog';
export class CleanupSessions extends Task {
constructor() {
super();
this.name = 'cleanupSessions';
this.description = 'Clean up expired sessions';
this.frequency = 1000 * 60 * 60; // Every hour
this.queue = 'maintenance';
}
async run() {
const cutoff = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7); // 7 days
const deleted = await Session.destroy({
where: {
lastAccessedAt: {
[Op.lt]: cutoff
}
}
});
api.log(\`Cleaned up \${deleted} expired sessions\`, 'info');
return { deleted };
}
}
export class CleanupLogs extends Task {
constructor() {
super();
this.name = 'cleanupLogs';
this.description = 'Clean up old audit logs';
this.frequency = 1000 * 60 * 60 * 24; // Daily
this.queue = 'maintenance';
}
async run() {
const cutoff = new Date(Date.now() - 1000 * 60 * 60 * 24 * 90); // 90 days
const deleted = await AuditLog.destroy({
where: {
createdAt: {
[Op.lt]: cutoff
}
}
});
api.log(\`Cleaned up \${deleted} old audit logs\`, 'info');
return { deleted };
}
}
export class CacheMaintenance extends Task {
constructor() {
super();
this.name = 'cacheMaintenance';
this.description = 'Perform cache maintenance';
this.frequency = 1000 * 60 * 30; // Every 30 minutes
this.queue = 'maintenance';
}
async run() {
// Get cache statistics
const keys = await api.cache.keys();
const stats = {
totalKeys: keys.length,
expired: 0,
cleaned: 0
};
// Clean expired entries
for (const key of keys) {
const ttl = await api.cache.ttl(key);
if (ttl && ttl < 0) {
await api.cache.destroy(key);
stats.expired++;
}
}
api.log(\`Cache maintenance: \${stats.expired} expired keys removed\`, 'info');
return stats;
}
}
`
},
{
path: 'tasks/reports.ts',
content: `import { Task, api } from 'actionhero';
import { User } from '../models/User';
import { Order } from '../models/Order';
import { Op } from 'sequelize';
export class DailyReport extends Task {
constructor() {
super();
this.name = 'dailyReport';
this.description = 'Generate daily usage report';
this.frequency = 1000 * 60 * 60 * 24; // Daily
this.queue = 'reports';
}
async run() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
// Gather statistics
const newUsers = await User.count({
where: {
createdAt: {
[Op.gte]: yesterday,
[Op.lt]: today
}
}
});
const activeUsers = await User.count({
where: {
lastLoginAt: {
[Op.gte]: yesterday,
[Op.lt]: today
}
}
});
const orders = await Order.findAll({
where: {
createdAt: {
[Op.gte]: yesterday,
[Op.lt]: today
}
},
attributes: [
[api.sequelize.fn('COUNT', api.sequelize.col('id')), 'count'],
[api.sequelize.fn('SUM', api.sequelize.col('total')), 'revenue']
]
});
const report = {
date: yesterday.toISOString().split('T')[0],
metrics: {
newUsers,
activeUsers,
orders: orders[0]?.get('count') || 0,
revenue: orders[0]?.get('revenue') || 0
}
};
// Send report email
await api.tasks.enqueue('sendEmail', {
to: process.env.ADMIN_EMAIL || 'admin@example.com',
subject: \`Daily Report - \${report.date}\`,
template: 'daily-report',
data: report
}, 'email');
api.log(\`Daily report generated for \${report.date}\`, 'info');
return report;
}
}
export class WeeklyAnalytics extends Task {
constructor() {
super();
this.name = 'weeklyAnalytics';
this.description = 'Generate weekly analytics report';
this.frequency = 1000 * 60 * 60 * 24 * 7; // Weekly
this.queue = 'reports';
}
async run() {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
// Complex analytics logic here
const analytics = {
userGrowth: await this.calculateUserGrowth(weekAgo),
engagement: await this.calculateEngagement(weekAgo),
revenue: await this.calculateRevenue(weekAgo),
performance: await this.getPerformanceMetrics()
};
// Store analytics
await api.cache.save('analytics:weekly:latest', analytics, 1000 * 60 * 60 * 24 * 8);
// Send to analytics service
await api.tasks.enqueue('sendAnalytics', {
type: 'weekly',
data: analytics
}, 'analytics');
return analytics;
}
private async calculateUserGrowth(since: Date) {
// Implementation
return { new: 0, active: 0, churn: 0 };
}
private async calculateEngagement(since: Date) {
// Implementation
return { sessions: 0, avgDuration: 0, bounceRate: 0 };
}
private async calculateRevenue(since: Date) {
// Implementation
return { total: 0, average: 0, growth: 0 };
}
private async getPerformanceMetrics() {
// Implementation
return { responseTime: 0, errorRate: 0, uptime: 0 };
}
}
`
},
{
path: 'middleware/auth.ts',
content: `import { api, Action } from 'actionhero';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export class AuthMiddleware {
name = 'auth';
global = false;
priority = 1000;
async preProcessor(data: any) {
const { connection } = data;
const token = this.extractToken(connection);
if (!token) {
throw new Error('No authentication token provided');
}
try {
// Verify token
const decoded = jwt.verify(token, JWT_SECRET) as any;
// Check if token is cached (not revoked)
const cachedToken = await api.cache.load(\`auth:token:\${decoded.id}\`);
if (cachedToken !== token) {
throw new Error('Token has been revoked');
}
// Attach user to session
data.session = { user: decoded };
// Log access
await api.tasks.enqueue('logAccess', {
userId: decoded.id,
action: data.actionTemplate.name,
ip: connection.remoteIP,
timestamp: new Date()
}, 'audit');
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token has expired');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
private extractToken(connection: any): string | null {
// Check Authorization header
if (connection.rawConnection.req?.headers?.authorization) {
const parts = connection.rawConnection.req.headers.authorization.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
return parts[1];
}
}
// Check query parameter
if (connection.params?.token) {
return connection.params.token;
}
// Check cookie
if (connection.rawConnection.req?.cookies?.token) {
return connection.rawConnection.req.cookies.token;
}
return null;
}
}
export class AdminOnlyMiddleware {
name = 'adminOnly';
global = false;
priority = 1001;
async preProcessor(data: any) {
const { user } = data.session || {};
if (!user) {
throw new Error('Authentication required');
}
if (user.role !== 'admin') {
throw new Error('Admin access required');
}
}
}
export class OwnerOrAdminMiddleware {
name = 'ownerOrAdmin';
global = false;
priority = 1001;
async preProcessor(data: any) {
const { user } = data.session || {};
const { id } = data.params;
if (!user) {
throw new Error('Authentication required');
}
if (user.role !== 'admin' && user.id !== parseInt(id)) {
throw new Error('Access denied');
}
}
}
`
},
{
path: 'middleware/rate-limit.ts',
content: `import { api } from 'actionhero';
export class RateLimitMiddleware {
name = 'rateLimit';
global = true;
priority = 500;
// Rate limit configuration
limits = {
default: { window: 60000, max: 100 }, // 100 requests per minute
auth: { window: 300000, max: 5 }, // 5 auth attempts per 5 minutes
api: { window: 60000, max: 1000 }, // 1000 API calls per minute
heavy: { window: 300000, max: 10 } // 10 heavy operations per 5 minutes
};
async preProcessor(data: any) {
const { connection, actionTemplate } = data;
const key = \`rateLimit:\${connection.remoteIP}:\${actionTemplate.name}\`;
// Get rate limit for this action
const limitType = actionTemplate.rateLimit || 'default';
const limit = this.limits[limitType] || this.limits.default;
// Get current count
const count = await api.cache.load(key) || 0;
if (count >= limit.max) {
const error = new Error('Rate limit exceeded');
error['code'] = 'RATE_LIMIT_EXCEEDED';
error['statusCode'] = 429;
error['limit'] = limit.max;
error['window'] = limit.window;
throw error;
}
// Increment counter
await api.cache.save(
key,
count + 1,
limit.window,
{ increment: true }
);
// Add rate limit headers
connection.rawConnection.responseHeaders = {
...connection.rawConnection.responseHeaders,
'X-RateLimit-Limit': limit.max,
'X-RateLimit-Remaining': limit.max - count - 1,
'X-RateLimit-Reset': new Date(Date.now() + limit.window).toISOString()
};
}
}
`
},
{
path: 'middleware/validation.ts',
content: `import { api } from 'actionhero';
export class ValidationMiddleware {
name = 'validation';
global = true;
priority = 700;
validators = {
email: (value: string) => {
const regex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
if (!regex.test(value)) {
throw new Error('Invalid email format');
}
},
'min:6': (value: string) => {
if (value.length < 6) {
throw new Error('Value must be at least 6 characters');
}
},
uuid: (value: string) => {
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!regex.test(value)) {
throw new Error('Invalid UUID format');
}
},
numeric: (value: any) => {
if (isNaN(value)) {
throw new Error('Value must be numeric');
}
},
boolean: (value: any) => {
if (typeof value !== 'boolean' && value !== 'true' && value !== 'false') {
throw new Error('Value must be boolean');
}
},
date: (value: string) => {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error('Invalid date format');
}
},
json: (value: string) => {
try {
JSON.parse(value);
} catch {
throw new Error('Invalid JSON format');
}
}
};
async preProcessor(data: any) {
const { params, actionTemplate } = data;
if (!actionTemplate.inputs) return;
for (const [param, config] of Object.entries(actionTemplate.inputs)) {
const value = params[param];
// Check required
if (config.required && (value === undefined || value === null || value === '')) {
throw new Error(\`Parameter '\${param}' is required\`);
}
// Skip validation if not provided and not required
if (!config.required && (value === undefined || value === null)) {
continue;
}
// Apply validators
if (config.validator) {
const validators = Array.isArray(config.validator)
? config.validator
: [config.validator];
for (const validator of validators) {
if (this.validators[validator]) {
try {
this.validators[validator](value);
} catch (error) {
throw new Error(\`Parameter '\${param}': \${error.message}\`);
}
}
}
}
}
}
}
`
},
{
path: 'initializers/database.ts',
content: `import { Initializer, api } from 'actionhero';
import { Sequelize } from 'sequelize-typescript';
import { User } from '../models/User';
import { Product } from '../models/Product';
import { Order } from '../models/Order';
import { Session } from '../models/Session';
import { AuditLog } from '../models/AuditLog';
export class Database extends Initializer {
constructor() {
super();
this.name = 'database';
this.loadPriority = 100;
this.startPriority = 100;
this.stopPriority = 100;
}
async initialize() {
const config = {
database: process.env.DB_NAME || 'actionhero_dev',
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASS || 'password',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
dialect: 'postgres' as const,
logging: process.env.NODE_ENV === 'development' ? console.log : false,
models: [User, Product, Order, Session, AuditLog]
};
api.sequelize = new Sequelize(config);
}
async start() {
try {
await api.sequelize.authenticate();
api.log('Database connection established', 'info');
// Sync models in development
if (process.env.NODE_ENV === 'development') {
await api.sequelize.sync({ alter: true });
api.log('Database models synchronized', 'info');
}
} catch (error) {
api.log(\`Database connection failed: \${error.message}\`, 'error');
throw error;
}
}
async stop() {
if (api.sequelize) {
await api.sequelize.close();
api.log('Database connection closed', 'info');
}
}
}
`
},
{
path: 'initializers/logger.ts',
content: `import { Initializer, api, log } from 'actionhero';
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
export class Logger extends Initializer {
constructor() {
super();
this.name = 'logger';
this.loadPriority = 10;
}
async initialize() {
const logDir = process.env.LOG_DIR || './logs';
// Console transport
const consoleTransport = new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
const metaString = Object.keys(meta).length ? JSON.stringify(meta) : '';
return \`\${timestamp} [\${level}]: \${message} \${metaString}\`;
})
)
});
// File transport for errors
const errorFileTransport = new DailyRotateFile({
filename: \`\${logDir}/error-%DATE%.log\`,
datePattern: 'YYYY-MM-DD',
level: 'error',
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
)
});
// File transport for all logs
const combinedFileTransport = new DailyRotateFile({
filename: \`\${logDir}/combined-%DATE%.log\`,
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
)
});
// Create winston logger
api.winston = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
transports: [
consoleTransport,
errorFileTransport,
combinedFileTransport
]
});
// Override ActionHero's log method
const originalLog = api.log;
api.log = (message: string, severity: string = 'info', data?: any) => {
api.winston.log(severity, message, data);
originalLog.call(api, message, severity, data);
};
}
}
`
},
{
path: 'models/User.ts',
content: `import {
Table,
Column,
Model,
DataType,
CreatedAt,
UpdatedAt,
HasMany,
BeforeCreate,
BeforeUpdate
} from 'sequelize-typescript';
import bcrypt from 'bcrypt';
import { Order } from './Order';
@Table({
tableName: 'users',
timestamps: true
})
export class User extends Model {
@Column({
type: DataType.INTEGER,
primaryKey: true,
autoIncrement: true
})
id!: number;
@Column({
type: DataType.STRING,
allowNull: false
})
name!: string;
@Column({
type: DataType.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
})
email!: string;
@Column({
type: DataType.STRING,
allowNull: false
})
password!: string;
@Column({
type: DataType.ENUM('user', 'admin'),
defaultValue: 'user'
})
role!: 'user' | 'admin';
@Column({
type: DataType.DATE,
allowNull: true
})
lastLoginAt?: Date;
@Column({
type: DataType.BOOLEAN,
defaultValue: true
})
isActive!: boolean;
@Column({
type: DataType.JSONB,
defaultValue: {}
})
metadata!: any;
@CreatedAt
createdAt!: Date;
@UpdatedAt
updatedAt!: Date;
@HasMany(() => Order)
orders!: Order[];
// Instance methods
async validatePassword(password: string): Promise<boolean> {
return bcrypt.compare(password, this.password);
}
toSafeJSON() {
const values = this.toJSON() as any;
delete values.password;
return values;
}
}
`
},
{
path: 'models/Product.ts',
content: `import {
Table,
Column,
Model,
DataType,
CreatedAt,
UpdatedAt,
HasMany,
Index
} from 'sequelize-typescript';
import { Order } from './Order';
@Table({
tableName: 'products',
timestamps: true
})
export class Product extends Model {
@Column({
type: DataType.INTEGER,
primaryKey: true,
autoIncrement: true
})
id!: number;
@Column({
type: DataType.STRING,
allowNull: false
})
name!: string;
@Column({
type: DataType.TEXT,
allowNull: true
})
description?: string;
@Column({
type: DataType.STRING,
allowNull: false,
unique: true
})
@Index
sku!: string;
@Column({
type: DataType.DECIMAL(10, 2),
allowNull: false,
validate: {
min: 0
}
})
price!: number;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
validate: {
min: 0
}
})
stock!: number;
@Column({
type: DataType.STRING,
allowNull: true
})
@Index
category?: string;
@Column({
type: DataType.ARRAY(DataType.STRING),
defaultValue: []
})
tags!: string[];
@Column({
type: DataType.JSONB,
defaultValue: {}
})
attributes!: any;
@Column({
type: DataType.BOOLEAN,
defaultValue: true
})
@Index
isActive!: boolean;
@CreatedAt
createdAt!: Date;
@UpdatedAt
updatedAt!: Date;
// Associations would go here
// @HasMany(() => OrderItem)
// orderItems!: OrderItem[];
}
`
},
{
path: 'test/actions/auth.test.ts',
content: `import { Process, env, id, actionhero } from 'actionhero';
import { api } from 'actionhero';
const actionhero = new Process();
describe('Auth Actions', () => {
beforeAll(async () => {
await actionhero.start();
});
afterAll(async () => {
await actionhero.stop();
});
describe('auth:register', () => {
it('should register a new user', async () => {
const response = await api.specHelper.runAction('auth:register', {
name: 'Test User',
email: 'test@example.com',
password: 'password123'
});
expect(response.token).toBeDefined();
expect(response.user).toBeDefined();
expect(response.user.email).toBe('test@example.com');
expect(response.user.password).toBeUndefined();
});
it('should fail with existing email', async () => {
// First registration
await api.specHelper.runAction('auth:register', {
name: 'Test User',
email: 'duplicate@example.com',
password: 'password123'
});
// Duplicate registration
const response = await api.specHelper.runAction('auth:register', {
name: 'Another User',
email: 'duplicate@example.com',
password: 'password456'
});
expect(response.error).toBe('User already exists with this email');
});
it('should validate email format', async () => {
const response = await api.specHelper.runAction('auth:register', {
name: 'Test User',
email: 'invalid-email',
password: 'password123'
});
expect(response.error).toContain('Invalid email format');
});
});
describe('auth:login', () => {
beforeEach(async () => {
await api.specHelper.runAction('auth:register', {
name: 'Login Test User',
email: 'login@example.com',
password: 'correctpassword'
});
});
it('should login with correct credentials', async () => {
const response = await api.specHelper.runAction('auth:login', {
email: 'login@example.com',
password: 'correctpassword'
});
expect(response.token).toBeDefined();
expect(response.user).toBeDefined();
expect(response.user.email).toBe('lo