@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
1,921 lines (1,664 loc) • 64.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.foaltsTemplate = void 0;
exports.foaltsTemplate = {
id: 'foalts',
name: 'foalts',
displayName: 'FoalTS',
description: 'TypeScript-first Node.js framework with decorators, CLI tools, built-in auth, and comprehensive features',
language: 'typescript',
framework: 'foalts',
version: '4.2.0',
tags: ['nodejs', 'foalts', 'api', 'rest', 'graphql', 'typescript', 'decorators', 'cli', 'auth', 'orm'],
port: 3001,
dependencies: {},
features: [
'decorators',
'cli-generator',
'authentication',
'authorization',
'openapi',
'validation',
'orm-typeorm',
'graphql',
'websocket',
'file-upload',
'scheduled-jobs',
'shell-scripts',
'testing'
],
files: {
// Package configuration
'package.json': `{
"name": "{{projectName}}",
"version": "1.0.0",
"description": "FoalTS application with TypeScript-first approach",
"main": "build/index.js",
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"build": "foal rmdir build && tsc",
"start": "node ./build/index.js",
"dev": "npm run build && concurrently -r \"tsc -w\" \"supervisor -w ./build,./config -e js,yml,json --no-restart-on error ./build/index.js\"",
"build:test": "foal rmdir build && tsc -p tsconfig.test.json",
"start:test": "mocha --file \"./build/test.js\" \"./build/**/*.spec.js\"",
"test": "npm run build:test && npm run start:test",
"migrations": "foal run-script build/scripts/migrate",
"makemigrations": "foal run-script build/scripts/create-migration",
"revertmigration": "foal run-script build/scripts/revert-migration",
"lint": "eslint --ext .ts src",
"lint:fix": "eslint --ext .ts --fix src",
"format": "prettier --write \"src/**/*.ts\"",
"generate:openapi": "foal generate openapi",
"generate:graphql-schema": "foal run-script build/scripts/generate-graphql-schema",
"docker:build": "docker build -t {{projectName}} .",
"docker:run": "docker run -p 3001:3001 {{projectName}}"
},
"dependencies": {
"@foal/core": "^4.2.0",
"@foal/jwt": "^4.2.0",
"@foal/password": "^4.2.0",
"@foal/social": "^4.2.0",
"@foal/storage": "^4.2.0",
"@foal/typeorm": "^4.2.0",
"@foal/graphql": "^4.2.0",
"@foal/socket.io": "^4.2.0",
"@foal/cli": "^4.2.0",
"@foal/swagger": "^4.2.0",
"@foal/ajv": "^4.2.0",
"@foal/redis": "^4.2.0",
"@foal/aws-s3": "^4.2.0",
"typeorm": "~0.3.17",
"sqlite3": "^5.1.6",
"pg": "^8.11.3",
"mysql2": "^3.6.5",
"redis": "^4.6.10",
"class-validator": "^0.14.0",
"class-transformer": "^0.5.1",
"graphql": "^16.8.1",
"apollo-server-express": "^3.13.0",
"socket.io": "^4.6.2",
"node-cron": "^3.0.3",
"bcrypt": "^5.1.1",
"jsonwebtoken": "^9.0.2",
"helmet": "^7.1.0",
"cors": "^2.8.5",
"compression": "^1.7.4",
"express-rate-limit": "^7.1.5",
"winston": "^3.11.0",
"dotenv": "^16.3.1",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.7",
"bull": "^4.12.0",
"@types/bull": "^4.10.0"
},
"devDependencies": {
"@types/node": "^20.10.4",
"@types/mocha": "^10.0.6",
"@types/supertest": "^2.0.16",
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/compression": "^1.7.5",
"@types/jsonwebtoken": "^9.0.5",
"@types/multer": "^1.4.11",
"@types/nodemailer": "^6.4.14",
"@types/node-cron": "^3.0.11",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.1.0",
"typescript": "~5.3.2",
"concurrently": "^8.2.2",
"supervisor": "^0.12.0",
"mocha": "^10.2.0",
"supertest": "^6.3.3",
"chai": "^4.3.10",
"@types/chai": "^4.3.11"
}
}`,
// TypeScript configuration
'tsconfig.json': `{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}`,
// Test TypeScript configuration
'tsconfig.test.json': `{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}`,
// Main application entry
'src/index.ts': `// FoalTS
import { createApp } from '@foal/core';
import { createConnection } from 'typeorm';
// App
import { AppController } from './app/app.controller';
import { dataSource } from './db';
async function main() {
// Initialize the database connection
await dataSource.initialize();
const app = await createApp(AppController);
const port = process.env.PORT || 3001;
app.listen(port, () => {
console.log(\`Listening on port \${port}...\`);
console.log(\`API documentation available at http://localhost:\${port}/swagger\`);
console.log(\`GraphQL playground available at http://localhost:\${port}/graphql\`);
});
}
main()
.catch(err => {
console.error(err);
process.exit(1);
});`,
// Test setup
'src/test.ts': `// Test setup file
import { createConnection, getConnection } from 'typeorm';
export const testDataSource = {
type: 'sqlite',
database: ':memory:',
dropSchema: true,
entities: ['build/app/**/*.entity.js'],
migrations: ['build/migrations/*.js'],
synchronize: true,
};
before(async () => {
await createConnection(testDataSource as any);
});
after(async () => {
await getConnection().close();
});`,
// Database configuration
'src/db.ts': `import { DataSource } from 'typeorm';
import { config } from '@foal/core';
export const dataSource = new DataSource({
type: config.get('database.type') as any,
// Common options
database: config.get('database.database'),
synchronize: config.get('database.synchronize'),
logging: config.get('database.logging'),
// Additional options based on database type
...(config.get('database.type') === 'postgres' && {
host: config.get('database.host'),
port: config.get('database.port'),
username: config.get('database.username'),
password: config.get('database.password'),
}),
entities: ['build/app/**/*.entity.js'],
migrations: ['build/migrations/*.js'],
cli: {
migrationsDir: 'src/migrations'
}
});`,
// Main App Controller
'src/app/app.controller.ts': `import {
controller,
IAppController,
Get,
render,
dependency,
Config,
ApiInfo,
ApiServer,
HttpResponseOK
} from '@foal/core';
import { createConnection } from 'typeorm';
import { join } from 'path';
// Controllers
import { AuthController } from './controllers/auth.controller';
import { UserController } from './controllers/user.controller';
import { TodoController } from './controllers/todo.controller';
import { ApiController } from './controllers/api.controller';
import { GraphQLController } from './controllers/graphql.controller';
import { WebSocketController } from './controllers/websocket.controller';
// Hooks
import { JWTRequired } from '@foal/jwt';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
// Services
import { SchedulerService } from './services/scheduler.service';
@ApiInfo({
title: '{{projectName}} API',
version: '1.0.0',
description: 'FoalTS application with comprehensive features',
contact: {
name: 'API Support',
email: 'support@example.com'
}
})
@ApiServer({
url: '/api',
description: 'API server'
})
export class AppController implements IAppController {
subControllers = [
controller('/auth', AuthController),
controller('/api', ApiController),
controller('/graphql', GraphQLController),
controller('/ws', WebSocketController),
controller('/swagger', SwaggerController),
];
@dependency
scheduler: SchedulerService;
async init() {
// Start scheduled jobs
this.scheduler.start();
}
// Apply global middlewares
@Get('*')
applyMiddlewares(ctx: any) {
// Security headers
ctx.request.use(helmet({
contentSecurityPolicy: false, // Disable for GraphQL playground
}));
// CORS
ctx.request.use(cors({
origin: Config.get('cors.origin', '*'),
credentials: true
}));
// Compression
ctx.request.use(compression());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
ctx.request.use(limiter);
}
@Get('/')
index() {
return new HttpResponseOK({
name: '{{projectName}}',
version: '1.0.0',
endpoints: {
api: '/api',
auth: '/auth',
graphql: '/graphql',
websocket: '/ws',
swagger: '/swagger',
health: '/health'
}
});
}
@Get('/health')
health() {
return new HttpResponseOK({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: Config.get('env', 'development')
});
}
}
// Swagger Controller
import { SwaggerController as FoalSwaggerController } from '@foal/swagger';
class SwaggerController extends FoalSwaggerController {
options = {
controllerClass: AppController
};
}`,
// API Controller
'src/app/controllers/api.controller.ts': `import { controller, ApiInfo, ApiServer } from '@foal/core';
import { UserController } from './user.controller';
import { TodoController } from './todo.controller';
@ApiInfo({
title: 'API v1',
version: '1.0.0'
})
@ApiServer({
url: '/api/v1'
})
export class ApiController {
subControllers = [
controller('/users', UserController),
controller('/todos', TodoController),
];
}`,
// Authentication Controller
'src/app/controllers/auth.controller.ts': `import {
Context,
HttpResponseOK,
HttpResponseUnauthorized,
HttpResponseBadRequest,
Post,
ValidateBody,
ApiOperation,
ApiResponse,
dependency,
Config
} from '@foal/core';
import { getSecretOrPublicKey } from '@foal/jwt';
import { sign } from 'jsonwebtoken';
import { AuthService } from '../services/auth.service';
import { EmailService } from '../services/email.service';
const credentialsSchema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 8 }
},
required: ['email', 'password'],
additionalProperties: false
};
const registerSchema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 8 },
name: { type: 'string', minLength: 1 }
},
required: ['email', 'password', 'name'],
additionalProperties: false
};
export class AuthController {
@dependency
authService: AuthService;
@dependency
emailService: EmailService;
@Post('/register')
@ValidateBody(registerSchema)
@ApiOperation({
summary: 'Register a new user',
description: 'Create a new user account with email verification'
})
@ApiResponse(201, { description: 'User registered successfully' })
@ApiResponse(400, { description: 'Validation error or user already exists' })
async register(ctx: Context) {
const { email, password, name } = ctx.request.body;
try {
const { user, verificationToken } = await this.authService.register({
email,
password,
name
});
// Send verification email
await this.emailService.sendVerificationEmail(email, verificationToken);
// Generate JWT token
const token = sign(
{ id: user.id, email: user.email },
getSecretOrPublicKey(),
{ expiresIn: '7d' }
);
return new HttpResponseOK({
user: {
id: user.id,
email: user.email,
name: user.name
},
token
});
} catch (error: any) {
return new HttpResponseBadRequest({
message: error.message
});
}
}
@Post('/login')
@ValidateBody(credentialsSchema)
@ApiOperation({
summary: 'Login user',
description: 'Authenticate user and return JWT token'
})
@ApiResponse(200, { description: 'Login successful' })
@ApiResponse(401, { description: 'Invalid credentials' })
async login(ctx: Context) {
const { email, password } = ctx.request.body;
const user = await this.authService.login(email, password);
if (!user) {
return new HttpResponseUnauthorized({
message: 'Invalid email or password'
});
}
// Generate JWT token
const token = sign(
{ id: user.id, email: user.email, role: user.role },
getSecretOrPublicKey(),
{ expiresIn: '7d' }
);
return new HttpResponseOK({
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
token
});
}
@Post('/verify-email/:token')
@ApiOperation({
summary: 'Verify email address',
description: 'Verify user email using verification token'
})
async verifyEmail(ctx: Context) {
const { token } = ctx.request.params;
try {
await this.authService.verifyEmail(token);
return new HttpResponseOK({
message: 'Email verified successfully'
});
} catch (error: any) {
return new HttpResponseBadRequest({
message: error.message
});
}
}
@Post('/forgot-password')
@ValidateBody({
type: 'object',
properties: {
email: { type: 'string', format: 'email' }
},
required: ['email']
})
@ApiOperation({
summary: 'Request password reset',
description: 'Send password reset email to user'
})
async forgotPassword(ctx: Context) {
const { email } = ctx.request.body;
try {
const resetToken = await this.authService.forgotPassword(email);
await this.emailService.sendPasswordResetEmail(email, resetToken);
return new HttpResponseOK({
message: 'Password reset email sent'
});
} catch (error: any) {
return new HttpResponseBadRequest({
message: error.message
});
}
}
@Post('/reset-password/:token')
@ValidateBody({
type: 'object',
properties: {
password: { type: 'string', minLength: 8 }
},
required: ['password']
})
@ApiOperation({
summary: 'Reset password',
description: 'Reset user password using reset token'
})
async resetPassword(ctx: Context) {
const { token } = ctx.request.params;
const { password } = ctx.request.body;
try {
await this.authService.resetPassword(token, password);
return new HttpResponseOK({
message: 'Password reset successfully'
});
} catch (error: any) {
return new HttpResponseBadRequest({
message: error.message
});
}
}
}`,
// User Controller
'src/app/controllers/user.controller.ts': `import {
Context,
Delete,
Get,
HttpResponseOK,
HttpResponseNotFound,
HttpResponseForbidden,
Patch,
UserRequired,
ValidateBody,
ValidatePathParam,
ValidateQueryParam,
ApiOperation,
ApiResponse,
dependency,
Hook
} from '@foal/core';
import { JWTRequired } from '@foal/jwt';
import { fetchUser } from '@foal/typeorm';
import { User } from '../entities/user.entity';
import { UserService } from '../services/user.service';
const userUpdateSchema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' }
},
additionalProperties: false
};
@Hook(JWTRequired())
@Hook(fetchUser(User))
export class UserController {
@dependency
userService: UserService;
@Get('/me')
@ApiOperation({
summary: 'Get current user',
description: 'Get the authenticated user profile'
})
@ApiResponse(200, {
description: 'Current user profile',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/User'
}
}
}
})
@UserRequired()
async getMe(ctx: Context<User>) {
return new HttpResponseOK({
id: ctx.user.id,
email: ctx.user.email,
name: ctx.user.name,
role: ctx.user.role,
isEmailVerified: ctx.user.isEmailVerified,
createdAt: ctx.user.createdAt,
updatedAt: ctx.user.updatedAt
});
}
@Get('/')
@ValidateQueryParam('page', { type: 'integer', minimum: 1 }, { required: false })
@ValidateQueryParam('limit', { type: 'integer', minimum: 1, maximum: 100 }, { required: false })
@ValidateQueryParam('search', { type: 'string' }, { required: false })
@ApiOperation({
summary: 'Get all users',
description: 'Get paginated list of users (admin only)'
})
@UserRequired()
async getAllUsers(ctx: Context<User>) {
// Check if user is admin
if (ctx.user.role !== 'admin') {
return new HttpResponseForbidden({
message: 'Admin access required'
});
}
const page = ctx.request.query.page || 1;
const limit = ctx.request.query.limit || 10;
const search = ctx.request.query.search;
const result = await this.userService.getAllUsers({
page: Number(page),
limit: Number(limit),
search: search as string
});
return new HttpResponseOK(result);
}
@Get('/:id')
@ValidatePathParam('id', { type: 'string' })
@ApiOperation({
summary: 'Get user by ID',
description: 'Get a specific user by their ID'
})
@UserRequired()
async getUserById(ctx: Context<User>) {
const { id } = ctx.request.params;
const user = await this.userService.getUserById(id);
if (!user) {
return new HttpResponseNotFound({
message: 'User not found'
});
}
return new HttpResponseOK({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt
});
}
@Patch('/:id')
@ValidatePathParam('id', { type: 'string' })
@ValidateBody(userUpdateSchema)
@ApiOperation({
summary: 'Update user',
description: 'Update user profile'
})
@UserRequired()
async updateUser(ctx: Context<User>) {
const { id } = ctx.request.params;
// Users can only update their own profile unless admin
if (ctx.user.id !== id && ctx.user.role !== 'admin') {
return new HttpResponseForbidden({
message: 'Cannot update other users'
});
}
const user = await this.userService.updateUser(id, ctx.request.body);
if (!user) {
return new HttpResponseNotFound({
message: 'User not found'
});
}
return new HttpResponseOK({
message: 'User updated successfully',
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
});
}
@Delete('/:id')
@ValidatePathParam('id', { type: 'string' })
@ApiOperation({
summary: 'Delete user',
description: 'Delete a user (admin only)'
})
@UserRequired()
async deleteUser(ctx: Context<User>) {
const { id } = ctx.request.params;
// Only admins can delete users
if (ctx.user.role !== 'admin') {
return new HttpResponseForbidden({
message: 'Admin access required'
});
}
await this.userService.deleteUser(id);
return new HttpResponseOK({
message: 'User deleted successfully'
});
}
@Patch('/change-password')
@ValidateBody({
type: 'object',
properties: {
currentPassword: { type: 'string' },
newPassword: { type: 'string', minLength: 8 }
},
required: ['currentPassword', 'newPassword']
})
@ApiOperation({
summary: 'Change password',
description: 'Change the current user password'
})
@UserRequired()
async changePassword(ctx: Context<User>) {
const { currentPassword, newPassword } = ctx.request.body;
try {
await this.userService.changePassword(
ctx.user.id,
currentPassword,
newPassword
);
return new HttpResponseOK({
message: 'Password changed successfully'
});
} catch (error: any) {
return new HttpResponseBadRequest({
message: error.message
});
}
}
}`,
// Todo Controller
'src/app/controllers/todo.controller.ts': `import {
Context,
Delete,
Get,
HttpResponseCreated,
HttpResponseOK,
HttpResponseNotFound,
Patch,
Post,
UserRequired,
ValidateBody,
ValidatePathParam,
ValidateQueryParam,
ApiOperation,
ApiResponse,
dependency,
Hook
} from '@foal/core';
import { JWTRequired } from '@foal/jwt';
import { fetchUser } from '@foal/typeorm';
import { User } from '../entities/user.entity';
import { TodoService } from '../services/todo.service';
const todoSchema = {
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
description: { type: 'string' },
priority: { enum: ['low', 'medium', 'high'] },
dueDate: { type: 'string', format: 'date-time' }
},
required: ['title'],
additionalProperties: false
};
const todoUpdateSchema = {
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
description: { type: 'string' },
status: { enum: ['pending', 'in_progress', 'completed'] },
priority: { enum: ['low', 'medium', 'high'] },
dueDate: { type: 'string', format: 'date-time' }
},
additionalProperties: false
};
@Hook(JWTRequired())
@Hook(fetchUser(User))
@UserRequired()
export class TodoController {
@dependency
todoService: TodoService;
@Get('/')
@ValidateQueryParam('page', { type: 'integer', minimum: 1 }, { required: false })
@ValidateQueryParam('limit', { type: 'integer', minimum: 1, maximum: 100 }, { required: false })
@ValidateQueryParam('status', { enum: ['pending', 'in_progress', 'completed'] }, { required: false })
@ValidateQueryParam('priority', { enum: ['low', 'medium', 'high'] }, { required: false })
@ApiOperation({
summary: 'Get all todos',
description: 'Get paginated list of user todos with optional filtering'
})
async getAllTodos(ctx: Context<User>) {
const { page = 1, limit = 10, status, priority } = ctx.request.query;
const result = await this.todoService.getAllTodos({
userId: ctx.user.id,
page: Number(page),
limit: Number(limit),
status: status as string,
priority: priority as string
});
return new HttpResponseOK(result);
}
@Get('/:id')
@ValidatePathParam('id', { type: 'string' })
@ApiOperation({
summary: 'Get todo by ID',
description: 'Get a specific todo by its ID'
})
async getTodoById(ctx: Context<User>) {
const { id } = ctx.request.params;
const todo = await this.todoService.getTodoById(id, ctx.user.id);
if (!todo) {
return new HttpResponseNotFound({
message: 'Todo not found'
});
}
return new HttpResponseOK(todo);
}
@Post('/')
@ValidateBody(todoSchema)
@ApiOperation({
summary: 'Create todo',
description: 'Create a new todo item'
})
@ApiResponse(201, { description: 'Todo created successfully' })
async createTodo(ctx: Context<User>) {
const todoData = {
...ctx.request.body,
userId: ctx.user.id
};
const todo = await this.todoService.createTodo(todoData);
return new HttpResponseCreated(todo);
}
@Patch('/:id')
@ValidatePathParam('id', { type: 'string' })
@ValidateBody(todoUpdateSchema)
@ApiOperation({
summary: 'Update todo',
description: 'Update an existing todo'
})
async updateTodo(ctx: Context<User>) {
const { id } = ctx.request.params;
const todo = await this.todoService.updateTodo(
id,
ctx.user.id,
ctx.request.body
);
if (!todo) {
return new HttpResponseNotFound({
message: 'Todo not found'
});
}
return new HttpResponseOK({
message: 'Todo updated successfully',
todo
});
}
@Delete('/:id')
@ValidatePathParam('id', { type: 'string' })
@ApiOperation({
summary: 'Delete todo',
description: 'Delete a todo item'
})
async deleteTodo(ctx: Context<User>) {
const { id } = ctx.request.params;
const deleted = await this.todoService.deleteTodo(id, ctx.user.id);
if (!deleted) {
return new HttpResponseNotFound({
message: 'Todo not found'
});
}
return new HttpResponseOK({
message: 'Todo deleted successfully'
});
}
@Post('/bulk/delete')
@ValidateBody({
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' },
minItems: 1
}
},
required: ['ids']
})
@ApiOperation({
summary: 'Bulk delete todos',
description: 'Delete multiple todos at once'
})
async bulkDelete(ctx: Context<User>) {
const { ids } = ctx.request.body;
const count = await this.todoService.bulkDelete(ids, ctx.user.id);
return new HttpResponseOK({
message: \`\${count} todos deleted successfully\`
});
}
@Post('/bulk/update')
@ValidateBody({
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' },
minItems: 1
},
updates: todoUpdateSchema
},
required: ['ids', 'updates']
})
@ApiOperation({
summary: 'Bulk update todos',
description: 'Update multiple todos at once'
})
async bulkUpdate(ctx: Context<User>) {
const { ids, updates } = ctx.request.body;
const count = await this.todoService.bulkUpdate(ids, ctx.user.id, updates);
return new HttpResponseOK({
message: \`\${count} todos updated successfully\`
});
}
}`,
// GraphQL Controller
'src/app/controllers/graphql.controller.ts': `import { GraphQLController as BaseGraphQLController } from '@foal/graphql';
import { buildSchema } from 'graphql';
import { readFileSync } from 'fs';
import { join } from 'path';
import { dependency } from '@foal/core';
import { GraphQLResolvers } from '../services/graphql-resolvers.service';
export class GraphQLController extends BaseGraphQLController {
@dependency
resolvers: GraphQLResolvers;
schema = buildSchema(
readFileSync(join(__dirname, '../graphql/schema.graphql'), 'utf-8')
);
async getResolvers() {
return this.resolvers.getResolvers();
}
getPlaygroundOptions() {
return {
endpoint: '/graphql'
};
}
}`,
// WebSocket Controller
'src/app/controllers/websocket.controller.ts': `import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';
import { JWTRequired } from '@foal/jwt';
import { dependency } from '@foal/core';
import { WebSocketService } from '../services/websocket.service';
export class WebSocketController extends SocketIOController {
@dependency
wsService: WebSocketService;
@EventName('connection')
async onConnection(ctx: WebsocketContext) {
console.log('Client connected:', ctx.socket.id);
// Join user's personal room
const userId = ctx.session?.userId;
if (userId) {
ctx.socket.join(\`user:\${userId}\`);
}
}
@EventName('disconnect')
async onDisconnect(ctx: WebsocketContext) {
console.log('Client disconnected:', ctx.socket.id);
}
@EventName('join-room')
async joinRoom(ctx: WebsocketContext, payload: { roomId: string }) {
ctx.socket.join(payload.roomId);
return new WebsocketResponse('room-joined', { roomId: payload.roomId });
}
@EventName('leave-room')
async leaveRoom(ctx: WebsocketContext, payload: { roomId: string }) {
ctx.socket.leave(payload.roomId);
return new WebsocketResponse('room-left', { roomId: payload.roomId });
}
@EventName('todo-update')
async todoUpdate(ctx: WebsocketContext, payload: any) {
const userId = ctx.session?.userId;
if (userId) {
// Broadcast to user's room
ctx.socket.to(\`user:\${userId}\`).emit('todo-updated', payload);
}
}
@EventName('send-message')
async sendMessage(ctx: WebsocketContext, payload: { roomId: string; message: string }) {
// Broadcast message to room
ctx.socket.to(payload.roomId).emit('new-message', {
userId: ctx.session?.userId,
message: payload.message,
timestamp: new Date().toISOString()
});
}
}`,
// User Entity
'src/app/entities/user.entity.ts': `import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
Index
} from 'typeorm';
import { Todo } from './todo.entity';
export type UserRole = 'user' | 'admin';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
@Index()
email: string;
@Column()
password: string;
@Column()
name: string;
@Column({
type: 'simple-enum',
enum: ['user', 'admin'],
default: 'user'
})
role: UserRole;
@Column({ nullable: true })
avatar?: string;
@Column({ default: false })
isEmailVerified: boolean;
@Column({ nullable: true })
verificationToken?: string;
@Column({ nullable: true })
resetToken?: string;
@Column({ type: 'timestamp', nullable: true })
resetTokenExpiry?: Date;
@Column('simple-array', { default: '' })
refreshTokens: string[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Todo, todo => todo.user)
todos: Todo[];
}`,
// Todo Entity
'src/app/entities/todo.entity.ts': `import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
Index
} from 'typeorm';
import { User } from './user.entity';
export type TodoStatus = 'pending' | 'in_progress' | 'completed';
export type TodoPriority = 'low' | 'medium' | 'high';
@Entity()
export class Todo {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column({ nullable: true })
description?: string;
@Column({
type: 'simple-enum',
enum: ['pending', 'in_progress', 'completed'],
default: 'pending'
})
@Index()
status: TodoStatus;
@Column({
type: 'simple-enum',
enum: ['low', 'medium', 'high'],
default: 'medium'
})
@Index()
priority: TodoPriority;
@Column({ type: 'timestamp', nullable: true })
dueDate?: Date;
@Column()
@Index()
userId: string;
@ManyToOne(() => User, user => user.todos, { onDelete: 'CASCADE' })
user: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}`,
// Auth Service
'src/app/services/auth.service.ts': `import { hashPassword, verifyPassword } from '@foal/password';
import { generateToken } from '@foal/core';
import { User } from '../entities/user.entity';
import { dataSource } from '../../db';
export class AuthService {
private userRepository = dataSource.getRepository(User);
async register(data: {
email: string;
password: string;
name: string;
}) {
// Check if user already exists
const existingUser = await this.userRepository.findOne({
where: { email: data.email }
});
if (existingUser) {
throw new Error('User already exists');
}
// Hash password
const hashedPassword = await hashPassword(data.password);
// Generate verification token
const verificationToken = generateToken();
// Create user
const user = this.userRepository.create({
email: data.email,
password: hashedPassword,
name: data.name,
verificationToken
});
await this.userRepository.save(user);
return { user, verificationToken };
}
async login(email: string, password: string) {
const user = await this.userRepository.findOne({
where: { email }
});
if (!user) {
return null;
}
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
return null;
}
return user;
}
async verifyEmail(token: string) {
const user = await this.userRepository.findOne({
where: { verificationToken: token }
});
if (!user) {
throw new Error('Invalid verification token');
}
user.isEmailVerified = true;
user.verificationToken = undefined;
await this.userRepository.save(user);
}
async forgotPassword(email: string) {
const user = await this.userRepository.findOne({
where: { email }
});
if (!user) {
throw new Error('User not found');
}
// Generate reset token
const resetToken = generateToken();
const resetTokenExpiry = new Date();
resetTokenExpiry.setHours(resetTokenExpiry.getHours() + 1); // 1 hour expiry
user.resetToken = resetToken;
user.resetTokenExpiry = resetTokenExpiry;
await this.userRepository.save(user);
return resetToken;
}
async resetPassword(token: string, newPassword: string) {
const user = await this.userRepository.findOne({
where: { resetToken: token }
});
if (!user) {
throw new Error('Invalid reset token');
}
if (user.resetTokenExpiry && user.resetTokenExpiry < new Date()) {
throw new Error('Reset token expired');
}
// Hash new password
user.password = await hashPassword(newPassword);
user.resetToken = undefined;
user.resetTokenExpiry = undefined;
await this.userRepository.save(user);
}
}`,
// User Service
'src/app/services/user.service.ts': `import { dataSource } from '../../db';
import { User } from '../entities/user.entity';
import { hashPassword, verifyPassword } from '@foal/password';
export class UserService {
private userRepository = dataSource.getRepository(User);
async getAllUsers(options: {
page: number;
limit: number;
search?: string;
}) {
const query = this.userRepository.createQueryBuilder('user');
if (options.search) {
query.where(
'user.name LIKE :search OR user.email LIKE :search',
{ search: \`%\${options.search}%\` }
);
}
const [users, total] = await query
.skip((options.page - 1) * options.limit)
.take(options.limit)
.getManyAndCount();
return {
data: users.map(user => ({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt
})),
pagination: {
page: options.page,
limit: options.limit,
total,
pages: Math.ceil(total / options.limit)
}
};
}
async getUserById(id: string) {
return this.userRepository.findOne({ where: { id } });
}
async updateUser(id: string, data: Partial<User>) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
return null;
}
Object.assign(user, data);
return this.userRepository.save(user);
}
async deleteUser(id: string) {
await this.userRepository.delete(id);
}
async changePassword(userId: string, currentPassword: string, newPassword: string) {
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}
const isPasswordValid = await verifyPassword(currentPassword, user.password);
if (!isPasswordValid) {
throw new Error('Current password is incorrect');
}
user.password = await hashPassword(newPassword);
await this.userRepository.save(user);
}
async updateAvatar(userId: string, avatarUrl: string) {
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}
user.avatar = avatarUrl;
await this.userRepository.save(user);
return avatarUrl;
}
}`,
// Todo Service
'src/app/services/todo.service.ts': `import { dataSource } from '../../db';
import { Todo } from '../entities/todo.entity';
import { In } from 'typeorm';
export class TodoService {
private todoRepository = dataSource.getRepository(Todo);
async getAllTodos(options: {
userId: string;
page: number;
limit: number;
status?: string;
priority?: string;
}) {
const query = this.todoRepository
.createQueryBuilder('todo')
.where('todo.userId = :userId', { userId: options.userId });
if (options.status) {
query.andWhere('todo.status = :status', { status: options.status });
}
if (options.priority) {
query.andWhere('todo.priority = :priority', { priority: options.priority });
}
const [todos, total] = await query
.orderBy('todo.createdAt', 'DESC')
.skip((options.page - 1) * options.limit)
.take(options.limit)
.getManyAndCount();
return {
data: todos,
pagination: {
page: options.page,
limit: options.limit,
total,
pages: Math.ceil(total / options.limit)
}
};
}
async getTodoById(id: string, userId: string) {
return this.todoRepository.findOne({
where: { id, userId }
});
}
async createTodo(data: Partial<Todo>) {
const todo = this.todoRepository.create(data);
return this.todoRepository.save(todo);
}
async updateTodo(id: string, userId: string, data: Partial<Todo>) {
const todo = await this.todoRepository.findOne({
where: { id, userId }
});
if (!todo) {
return null;
}
Object.assign(todo, data);
return this.todoRepository.save(todo);
}
async deleteTodo(id: string, userId: string) {
const result = await this.todoRepository.delete({ id, userId });
return result.affected > 0;
}
async bulkDelete(ids: string[], userId: string) {
const result = await this.todoRepository.delete({
id: In(ids),
userId
});
return result.affected || 0;
}
async bulkUpdate(ids: string[], userId: string, updates: Partial<Todo>) {
const result = await this.todoRepository.update(
{ id: In(ids), userId },
updates
);
return result.affected || 0;
}
}`,
// Email Service
'src/app/services/email.service.ts': `import { Config } from '@foal/core';
import * as nodemailer from 'nodemailer';
export class EmailService {
private transporter: nodemailer.Transporter;
constructor() {
this.transporter = nodemailer.createTransport({
host: Config.get('email.host'),
port: Config.get('email.port'),
secure: Config.get('email.secure', false),
auth: {
user: Config.get('email.user'),
pass: Config.get('email.pass')
}
});
}
async sendVerificationEmail(email: string, token: string) {
const verificationUrl = \`\${Config.get('app.url')}/verify-email/\${token}\`;
await this.transporter.sendMail({
from: Config.get('email.from'),
to: email,
subject: 'Verify your email address',
html: \`
<h1>Email Verification</h1>
<p>Please click the link below to verify your email address:</p>
<a href="\${verificationUrl}">Verify Email</a>
<p>If you didn't request this, please ignore this email.</p>
\`
});
}
async sendPasswordResetEmail(email: string, token: string) {
const resetUrl = \`\${Config.get('app.url')}/reset-password/\${token}\`;
await this.transporter.sendMail({
from: Config.get('email.from'),
to: email,
subject: 'Password Reset Request',
html: \`
<h1>Password Reset</h1>
<p>You requested a password reset. Click the link below to reset your password:</p>
<a href="\${resetUrl}">Reset Password</a>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
\`
});
}
async sendWelcomeEmail(email: string, name: string) {
await this.transporter.sendMail({
from: Config.get('email.from'),
to: email,
subject: 'Welcome to {{projectName}}!',
html: \`
<h1>Welcome, \${name}!</h1>
<p>Thank you for joining {{projectName}}. We're excited to have you on board!</p>
<p>If you have any questions, feel free to reach out to our support team.</p>
\`
});
}
}`,
// Scheduler Service
'src/app/services/scheduler.service.ts': `import * as cron from 'node-cron';
import { Config } from '@foal/core';
import { dataSource } from '../../db';
import { Todo } from '../entities/todo.entity';
export class SchedulerService {
private tasks: cron.ScheduledTask[] = [];
start() {
// Clean up old completed todos every day at midnight
const cleanupTask = cron.schedule('0 0 * * *', async () => {
console.log('Running todo cleanup job...');
await this.cleanupOldTodos();
});
this.tasks.push(cleanupTask);
// Send reminder emails for due todos every hour
const reminderTask = cron.schedule('0 * * * *', async () => {
console.log('Running todo reminder job...');
await this.sendTodoReminders();
});
this.tasks.push(reminderTask);
// Database backup every day at 3 AM
const backupTask = cron.schedule('0 3 * * *', async () => {
console.log('Running database backup job...');
await this.backupDatabase();
});
this.tasks.push(backupTask);
console.log('Scheduler started with', this.tasks.length, 'jobs');
}
stop() {
this.tasks.forEach(task => task.stop());
this.tasks = [];
console.log('Scheduler stopped');
}
private async cleanupOldTodos() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const todoRepository = dataSource.getRepository(Todo);
const result = await todoRepository
.createQueryBuilder()
.delete()
.where('status = :status', { status: 'completed' })
.andWhere('updatedAt < :date', { date: thirtyDaysAgo })
.execute();
console.log(\`Cleaned up \${result.affected} old completed todos\`);
}
private async sendTodoReminders() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const todoRepository = dataSource.getRepository(Todo);
const dueTodos = await todoRepository
.createQueryBuilder('todo')
.leftJoinAndSelect('todo.user', 'user')
.where('todo.dueDate <= :tomorrow', { tomorrow })
.andWhere('todo.dueDate >= :now', { now: new Date() })
.andWhere('todo.status != :status', { status: 'completed' })
.getMany();
console.log(\`Found \${dueTodos.length} todos due soon\`);
// Here you would send reminder emails
// This is just a placeholder
for (const todo of dueTodos) {
console.log(\`Reminder: Todo "\${todo.title}" is due soon for user \${todo.user.email}\`);
}
}
private async backupDatabase() {
// Placeholder for database backup logic
// In a real application, you would implement actual backup logic here
console.log('Database backup completed');
}
}`,
// GraphQL Resolvers Service
'src/app/services/graphql-resolvers.service.ts': `import { dependency } from '@foal/core';
import { UserService } from './user.service';
import { TodoService } from './todo.service';
import { AuthService } from './auth.service';
export class GraphQLResolvers {
@dependency
userService: UserService;
@dependency
todoService: TodoService;
@dependency
authService: AuthService;
getResolvers() {
return {
// Queries
me: async (_: any, __: any, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return this.userService.getUserById(context.user.id);
},
user: async (_: any, args: { id: string }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return this.userService.getUserById(args.id);
},
users: async (_: any, args: { page?: number; limit?: number; search?: string }, context: any) => {
if (!context.user || context.user.role !== 'admin') {
throw new Error('Admin access required');
}
return this.userService.getAllUsers({
page: args.page || 1,
limit: args.limit || 10,
search: args.search
});
},
todos: async (_: any, args: { page?: number; limit?: number; status?: string; priority?: string }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return this.todoService.getAllTodos({
userId: context.user.id,
page: args.page || 1,
limit: args.limit || 10,
status: args.status,
priority: args.priority
});
},
todo: async (_: any, args: { id: string }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return this.todoService.getTodoById(args.id, context.user.id);
},
// Mutations
register: async (_: any, args: { email: string; password: string; name: string }) => {
const { user } = await this.authService.register(args);
return user;
},
login: async (_: any, args: { email: string; password: string }) => {
const user = await this.authService.login(args.email, args.password);
if (!user) {
throw new Error('Invalid credentials');
}
return user;
},
createTodo: async (_: any, args: any, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return this.todoService.createTodo({
...args.input,
userId: context.user.id
});
},
updateTodo: async (_: any, args: { id: string; input: any }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return this.todoService.updateTodo(args.id, context.user.id, args.input);
},
deleteTodo: async (_: any, args: { id: string }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
const deleted = await this.todoService.deleteTodo(args.id, context.user.id);
return deleted;
}
};
}
}`,
// WebSocket Service
'src/app/services/websocket.service.ts': `import { Server } from 'socket.io';
export class WebSocketService {
private io: Server;
setServer(io: Server) {
this.io = io;
}
emitToUser(userId: string, event: string, data: any) {
this.io.to(\`user:\${userId}\`).emit(event, data);
}
emitToRoom(roomId: string, event: string, data: any) {
this.io.to(roomId).emit(event, data);
}
broadcastToAll(event: string, data: any) {
this.io.emit(event, data);
}
}`,
// GraphQL Schema
'src/app/graphql/schema.graphql': `type User {
id: ID!
email: String!
name: String!
role: String!
avatar: String
isEmailVerified: Boolean!
createdAt: String!
updatedAt: String!
todos: [Todo!]!
}
type Todo {
id: ID!
title: String!
description: String
status: TodoStatus!
priority: TodoPriority!
dueDate: String
user: User!
createdAt: String!
updatedAt: String!
}
enum TodoStatus {
pending
in_progress
completed
}
enum TodoPriority {
low
medium
high
}
type PaginationInfo {
page: Int!
limit: Int!
total: Int!
pages: Int!
}
type UsersResponse {
data: [User!]!
pagination: PaginationInfo!
}
type TodosResponse {
data: [Todo!]!
pagination: PaginationInfo!
}
type Query {
me: User!
user(id: ID!): User
users(page: Int, limit: Int, search: String): UsersResponse!
todos(page: Int, limit: Int, status: TodoStatus, priority: TodoPriority): TodosResponse!
todo(id: ID!): Todo
}
input TodoInput {
title: String!
description: String
priority: TodoPriority
dueDate: String
}
input TodoUpdateInput {
title: String
description: String
status: TodoStatus
priority: TodoPriority
dueDate: String
}
type Mutation {
register(email: String!, password: String!, name: String!): User!
login(email: String!, password: String!): User!
createTodo(input: TodoInput!): Todo!
updateTodo(id: ID!, input: TodoUpdateInput!): Todo!
deleteTodo(id: ID!): Boolean!
}`,
// Database migration script
'src/scripts/create-migration.ts': `// Import the Config object to read configuration files
import { Config, createService } from '@foal/core';
import { execSync } from 'child_process';
export async function main() {
const migrationName = process.argv[2];
if (!migrationName) {
console.error('Please provide a migration name');
process.exit(1);
}
const command = \`npx typeorm migration:create src/migrations/\${migrationName}\`;
try {
execSync(command, { stdio: 'inherit' });
console.log(\`Migration \${migrationName} created successfully\`);
} catch (error) {
console.error('Failed to create migration:', error);
process.exit(1);
}
}`,
// Migration runner script
'src/scripts/migrate.ts': `// Import the dataSource to run migrations
import { dataSource } from '../db';
export async function main() {
try {
await dataSource.initialize();
awa