commit bc745ad172e4508c5b492e26cef942deb0208ec2 Author: lalitmohank Date: Tue Jan 6 20:13:19 2026 +0530 Initial commit - Node.js API server diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c13f20c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,48 @@ +# Git +.git +.gitignore + +# Dependencies (will be installed in Docker) +node_modules + +# Development files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*.sublime-* + +# Environment files (sensitive) +.env +.env.local +.env.*.local +*.env + +# Test files +coverage/ +.nyc_output/ +*.test.js +*.spec.js +__tests__/ + +# Documentation +README.md +CHANGELOG.md +docs/ + +# OS +.DS_Store +Thumbs.db + +# Misc +*.log +*.pid +*.seed +*.pid.lock + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d76c50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ + +# Environment files +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Test coverage +coverage/ +.nyc_output/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +dist/ +build/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c78608c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# ========================================== +# Production-Ready Multi-Stage Dockerfile +# ========================================== + +# Stage 1: Dependencies +FROM node:20-alpine AS deps +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production && npm cache clean --force + +# ========================================== +# Stage 2: Production Image +# ========================================== +FROM node:20-alpine AS runner +WORKDIR /app + +# Set production environment +ENV NODE_ENV=production +ENV PORT=8080 + +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nodeuser + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy application code +COPY --chown=nodeuser:nodejs . . + +# Switch to non-root user +USER nodeuser + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# Start the application +CMD ["node", "index.js"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc65d17 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# test-git-fcz5 + +Production-ready Node.js API server for testing git serverless deployment. + +## Features + +- ✅ Express.js REST API +- ✅ CRUD operations (Users & Tasks) +- ✅ Health checks (`/health`, `/ready`) +- ✅ Environment info endpoint (`/api/env`) +- ✅ Production-ready Dockerfile (multi-stage) +- ✅ Security middleware (Helmet, CORS) +- ✅ Request logging (Morgan) +- ✅ Gzip compression + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/` | Service info | +| GET | `/health` | Health check | +| GET | `/ready` | Readiness check | +| GET | `/api/env` | Environment variables (for testing) | +| GET | `/api/users` | List all users | +| GET | `/api/users/:id` | Get user by ID | +| POST | `/api/users` | Create user | +| PUT | `/api/users/:id` | Update user | +| DELETE | `/api/users/:id` | Delete user | +| GET | `/api/tasks` | List tasks (filter by userId, status) | +| POST | `/api/tasks` | Create task | +| PATCH | `/api/tasks/:id/status` | Update task status | +| ALL | `/api/echo` | Echo request details | + +## Local Development + +```bash +# Install dependencies +npm install + +# Run in development mode +npm run dev + +# Run in production mode +npm start +``` + +## Docker + +```bash +# Build image +docker build -t test-git-fcz5 . + +# Run container +docker run -p 8080:8080 test-git-fcz5 +``` + +## Testing API + +```bash +# Health check +curl http://localhost:8080/health + +# Get environment info +curl http://localhost:8080/api/env + +# List users +curl http://localhost:8080/api/users + +# Create user +curl -X POST http://localhost:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{"name": "Test User", "email": "test@example.com"}' + +# Create task +curl -X POST http://localhost:8080/api/tasks \ + -H "Content-Type: application/json" \ + -d '{"userId": "1", "title": "New task"}' + +# Update task status +curl -X PATCH http://localhost:8080/api/tasks/1/status \ + -H "Content-Type: application/json" \ + -d '{"status": "done"}' +``` + +## Backward Compatibility Testing + +This app uses the **OLD boltic.yaml format** (no `serverlessConfig` section). + +### Test Steps: +1. Deploy this app BEFORE new conductor code +2. Configure from UI: Scaling, Env, PortMap +3. Deploy new conductor code +4. Push a code change +5. Verify UI settings are preserved + +## License + +MIT + diff --git a/boltic.yaml b/boltic.yaml new file mode 100644 index 0000000..6a30e7c --- /dev/null +++ b/boltic.yaml @@ -0,0 +1,30 @@ +# Git Serverless Configuration - OLD FORMAT +# For backward compatibility testing (no serverlessConfig) + +app: "test-git-fcz5" +region: "asia-south1" +handler: "handler.handler" +language: "nodejs/20" + +settings: + nodejs: + common_js: false + +build: + builtin: dockerfile + ignorefile: .dockerignore + no_cache: false + args: + NODE_ENV: "production" + BUILD_VERSION: "1.0.0" + +# NOTE: NO serverlessConfig section! +# This is the OLD format for backward compatibility testing. +# +# TESTING STEPS: +# 1. Deploy this app BEFORE deploying new conductor code +# 2. Configure Scaling/Env/PortMap from UI +# 3. Deploy new conductor code +# 4. Push a code change to this repo +# 5. Verify all UI settings are preserved + diff --git a/index.js b/index.js new file mode 100644 index 0000000..673d3cf --- /dev/null +++ b/index.js @@ -0,0 +1,255 @@ +/** + * Production-Ready Node.js API Server + * For testing git serverless deployment + */ + +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); +const compression = require('compression'); +const { v4: uuidv4 } = require('uuid'); + +// Load environment variables +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 8080; +const NODE_ENV = process.env.NODE_ENV || 'development'; + +// =========================================== +// Middleware +// =========================================== +app.use(helmet()); // Security headers +app.use(cors()); // CORS support +app.use(compression()); // Gzip compression +app.use(morgan('combined')); // Request logging +app.use(express.json()); // Parse JSON bodies +app.use(express.urlencoded({ extended: true })); + +// =========================================== +// In-Memory Database (for testing) +// =========================================== +const database = { + users: [ + { id: '1', name: 'John Doe', email: 'john@example.com', createdAt: new Date().toISOString() }, + { id: '2', name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date().toISOString() }, + ], + tasks: [ + { id: '1', userId: '1', title: 'Complete project', status: 'pending', createdAt: new Date().toISOString() }, + { id: '2', userId: '1', title: 'Review PR', status: 'done', createdAt: new Date().toISOString() }, + ] +}; + +// =========================================== +// Health Check Routes +// =========================================== +app.get('/', (req, res) => { + res.json({ + service: 'test-git-fcz5', + version: '1.0.0', + status: 'healthy', + timestamp: new Date().toISOString(), + environment: NODE_ENV, + }); +}); + +app.get('/health', (req, res) => { + res.json({ + status: 'ok', + uptime: process.uptime(), + timestamp: new Date().toISOString(), + }); +}); + +app.get('/ready', (req, res) => { + res.json({ ready: true }); +}); + +// =========================================== +// Environment Info Route (for testing) +// =========================================== +app.get('/api/env', (req, res) => { + res.json({ + message: 'Environment variables (for testing config overrides)', + environment: { + NODE_ENV: process.env.NODE_ENV || 'not-set', + PORT: process.env.PORT || 'not-set', + // Boltic system variables + BOLT_APPLICATION_NAME: process.env.BOLT_APPLICATION_NAME || 'not-set', + BOLT_APPLICATION_SLUG: process.env.BOLT_APPLICATION_SLUG || 'not-set', + BOLT_APPLICATION_REGION_ID: process.env.BOLT_APPLICATION_REGION_ID || 'not-set', + // Custom env vars (set from UI or boltic.yaml) + API_KEY: process.env.API_KEY ? '***hidden***' : 'not-set', + DATABASE_URL: process.env.DATABASE_URL ? '***hidden***' : 'not-set', + CUSTOM_VAR: process.env.CUSTOM_VAR || 'not-set', + UI_TEST_VAR: process.env.UI_TEST_VAR || 'not-set', + }, + testInfo: { + purpose: 'Verify environment variables from UI and boltic.yaml', + configFormat: 'Check if serverlessConfig overrides are applied', + } + }); +}); + +// =========================================== +// Users API +// =========================================== +app.get('/api/users', (req, res) => { + res.json({ + success: true, + data: database.users, + total: database.users.length, + }); +}); + +app.get('/api/users/:id', (req, res) => { + const user = database.users.find(u => u.id === req.params.id); + if (!user) { + return res.status(404).json({ success: false, error: 'User not found' }); + } + res.json({ success: true, data: user }); +}); + +app.post('/api/users', (req, res) => { + const { name, email } = req.body; + if (!name || !email) { + return res.status(400).json({ success: false, error: 'Name and email are required' }); + } + + const newUser = { + id: uuidv4(), + name, + email, + createdAt: new Date().toISOString(), + }; + database.users.push(newUser); + + res.status(201).json({ success: true, data: newUser }); +}); + +app.put('/api/users/:id', (req, res) => { + const index = database.users.findIndex(u => u.id === req.params.id); + if (index === -1) { + return res.status(404).json({ success: false, error: 'User not found' }); + } + + database.users[index] = { ...database.users[index], ...req.body }; + res.json({ success: true, data: database.users[index] }); +}); + +app.delete('/api/users/:id', (req, res) => { + const index = database.users.findIndex(u => u.id === req.params.id); + if (index === -1) { + return res.status(404).json({ success: false, error: 'User not found' }); + } + + const deleted = database.users.splice(index, 1); + res.json({ success: true, data: deleted[0] }); +}); + +// =========================================== +// Tasks API +// =========================================== +app.get('/api/tasks', (req, res) => { + const { userId, status } = req.query; + let tasks = [...database.tasks]; + + if (userId) tasks = tasks.filter(t => t.userId === userId); + if (status) tasks = tasks.filter(t => t.status === status); + + res.json({ + success: true, + data: tasks, + total: tasks.length, + }); +}); + +app.post('/api/tasks', (req, res) => { + const { userId, title } = req.body; + if (!userId || !title) { + return res.status(400).json({ success: false, error: 'userId and title are required' }); + } + + const newTask = { + id: uuidv4(), + userId, + title, + status: 'pending', + createdAt: new Date().toISOString(), + }; + database.tasks.push(newTask); + + res.status(201).json({ success: true, data: newTask }); +}); + +app.patch('/api/tasks/:id/status', (req, res) => { + const { status } = req.body; + const validStatuses = ['pending', 'in-progress', 'done']; + + if (!status || !validStatuses.includes(status)) { + return res.status(400).json({ + success: false, + error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` + }); + } + + const task = database.tasks.find(t => t.id === req.params.id); + if (!task) { + return res.status(404).json({ success: false, error: 'Task not found' }); + } + + task.status = status; + res.json({ success: true, data: task }); +}); + +// =========================================== +// Echo/Debug Route (for testing) +// =========================================== +app.all('/api/echo', (req, res) => { + res.json({ + method: req.method, + path: req.path, + query: req.query, + headers: req.headers, + body: req.body, + timestamp: new Date().toISOString(), + }); +}); + +// =========================================== +// 404 Handler +// =========================================== +app.use((req, res) => { + res.status(404).json({ + success: false, + error: 'Not Found', + path: req.path, + }); +}); + +// =========================================== +// Error Handler +// =========================================== +app.use((err, req, res, next) => { + console.error('Error:', err); + res.status(500).json({ + success: false, + error: NODE_ENV === 'production' ? 'Internal Server Error' : err.message, + }); +}); + +// =========================================== +// Start Server +// =========================================== +app.listen(PORT, '0.0.0.0', () => { + console.log('========================================'); + console.log(`🚀 Server started successfully!`); + console.log(`📍 Environment: ${NODE_ENV}`); + console.log(`🌐 Listening on: http://0.0.0.0:${PORT}`); + console.log(`💚 Health check: http://0.0.0.0:${PORT}/health`); + console.log('========================================'); +}); + +module.exports = app; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..79c4f7c --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "test-git-fcz5", + "version": "1.0.0", + "description": "Production-ready Node.js API server for testing", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js", + "test": "jest --coverage", + "lint": "eslint .", + "build": "echo 'No build step required'" + }, + "keywords": [ + "nodejs", + "api", + "express", + "production", + "boltic" + ], + "author": "Boltic", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "compression": "^1.7.4", + "dotenv": "^16.3.1", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "jest": "^29.7.0", + "eslint": "^8.56.0" + } +} +