Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions apps/backend/lambdas/users/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Users Lambda

This Lambda function provides user management functionality.

## Setup

1. Install dependencies:
```bash
npm install
```

2. Apply database migration (if needed):
```bash
sqlite3 ../../../../db.sqlite < migration.sql
```

## Development

Start the development server:
```bash
npm run dev
```

The server will be available at:
- Handler: http://localhost:3000/users
- Swagger UI: http://localhost:3000/users/swagger
- Health Check: http://localhost:3000/users/health

## Testing

Run tests:
```bash
npm test
```

## API Endpoints

### PATCH /users/{userId}

Updates user information (partial update supported).

**Request Body:**
```json
{
"name": "string (optional)",
"isAdmin": "boolean (optional)"
}
```

**Success Response (200):**
```json
{
"id": 1,
"email": "user@example.com",
"name": "Updated Name",
"isAdmin": false
}
```

**Error Responses:**
- 400: Invalid request (invalid ID format, invalid field types, or no fields provided)
- 404: User not found

**Example:**
```bash
curl -X PATCH http://localhost:3000/users/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "isAdmin": true}'
```

## Building

Build for production:
```bash
npm run build
```

Package as Lambda:
```bash
npm run package
```

## Database Schema

The `user` table has the following columns:
- `id` (INTEGER, PRIMARY KEY, AUTO INCREMENT)
- `email` (VARCHAR, NOT NULL)
- `password` (VARCHAR, NOT NULL)
- `name` (VARCHAR, nullable)
- `isAdmin` (INTEGER, default 0, where 1 = true, 0 = false)
47 changes: 47 additions & 0 deletions apps/backend/lambdas/users/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Kysely, SqliteDialect, Generated } from 'kysely';
import Database from 'better-sqlite3';
import * as path from 'path';

// Database interface
interface UserTable {
id: Generated<number>;
email: string;
password: string;
name: string | null;
isAdmin: number;
}

interface DatabaseSchema {
user: UserTable;
}

// Get database connection
let dbInstance: Kysely<DatabaseSchema> | null = null;

export function getDatabase(): Kysely<DatabaseSchema> {
if (!dbInstance) {
// In production, this would come from environment variable
// For local development, we use the db.sqlite in the repo root
const dbPath = process.env.DB_PATH || path.join(__dirname, '../../../../db.sqlite');

const dialect = new SqliteDialect({
database: new Database(dbPath),
});

dbInstance = new Kysely<DatabaseSchema>({
dialect,
});
}

return dbInstance;
}

// Helper to convert SQLite integer to boolean
export function toBoolean(value: number | null | undefined): boolean {
return value === 1;
}

// Helper to convert boolean to SQLite integer
export function toInteger(value: boolean | undefined): number {
return value ? 1 : 0;
}
174 changes: 174 additions & 0 deletions apps/backend/lambdas/users/dev-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { handler } from './handler';
import { loadOpenApiSpec, getSwaggerHtml } from './swagger-utils';
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import { URL } from 'url';
import { APIGatewayProxyEvent } from 'aws-lambda';

const HANDLER_NAME = 'users';
const BASE_PATH = `/${HANDLER_NAME}`;

// Check if shared server exists, if not create it
const SHARED_SERVER_PORT = 3000;
const LOCK_FILE = path.join(__dirname, '..', '.dev-server.lock');

async function startOrJoinServer() {
// Try to register this handler with existing server
try {
const response = await fetch(`http://localhost:${SHARED_SERVER_PORT}/_register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
handlerName: HANDLER_NAME,
handlerPath: __dirname
})
});

if (response.ok) {
console.log(`Registered ${HANDLER_NAME} with existing dev server`);
console.log(`Handler available at: http://localhost:${SHARED_SERVER_PORT}${BASE_PATH}`);
console.log(`Swagger UI: http://localhost:${SHARED_SERVER_PORT}${BASE_PATH}/swagger`);
return;
}
} catch (err) {
// Server doesn't exist, we'll create it
}

// Create the shared server
const handlers = new Map();

const server = http.createServer(async (req, res) => {
try {
const chunks = [] as Buffer[];
req.on('data', (c) => chunks.push(c));
req.on('end', async () => {
const bodyRaw = Buffer.concat(chunks).toString('utf8');
const fullUrl = new URL(req.url || '/', `http://localhost:${SHARED_SERVER_PORT}`);

// Handle registration endpoint
if (fullUrl.pathname === '/_register' && req.method === 'POST') {
const { handlerName, handlerPath } = JSON.parse(bodyRaw);
try {
const handlerModule = require(path.join(handlerPath, 'handler.ts'));
const swaggerUtils = require(path.join(handlerPath, 'swagger-utils.ts'));
handlers.set(handlerName, { handler: handlerModule.handler, swaggerUtils, handlerPath });
res.statusCode = 200;
res.end('OK');
console.log(`Registered handler: ${handlerName}`);
} catch (err) {
res.statusCode = 500;
res.end('Failed to load handler');
}
return;
}

// Handle root route - show available handlers
if (fullUrl.pathname === '/' && req.method === 'GET') {
const handlerList = Array.from(handlers.keys()).map(name =>
`<li><a href="/${name}">${name}</a> - <a href="/${name}/swagger">Swagger UI</a></li>`
).join('');

const html = `<!DOCTYPE html>
<html><head><title>Lambda Dev Server</title></head>
<body>
<h1>Lambda Development Server</h1>
<h2>Available Handlers:</h2>
<ul>${handlerList}</ul>
</body></html>`;

res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(html);
return;
}

// Route to specific handler
const pathParts = fullUrl.pathname.split('/').filter(Boolean);
const handlerName = pathParts[0];

if (!handlers.has(handlerName)) {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: `Handler '${handlerName}' not found` }));
return;
}

const { handler: handlerFn, swaggerUtils } = handlers.get(handlerName);
const handlerPath = '/' + pathParts.slice(1).join('/');

// Handle Swagger routes for this handler
if (handlerPath === '/swagger.json' && req.method === 'GET') {
const spec = swaggerUtils.loadOpenApiSpec();
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(spec));
return;
}
if (handlerPath === '/swagger' && req.method === 'GET') {
const html = swaggerUtils.getSwaggerHtml(`/${handlerName}/swagger.json`);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(html);
return;
}

// Create Lambda event (compatible with both API Gateway and Function URL formats)
const event = {
// API Gateway format
body: bodyRaw || null,
headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : (v ?? '')])) as Record<string, string>,
httpMethod: (req.method || 'GET').toUpperCase(),
isBase64Encoded: false,
multiValueHeaders: {},
multiValueQueryStringParameters: null,
path: handlerPath || '/',
pathParameters: null,
queryStringParameters: Object.fromEntries(fullUrl.searchParams.entries()),
requestContext: {
// Function URL format
http: {
method: (req.method || 'GET').toUpperCase(),
path: handlerPath || '/',
protocol: 'HTTP/1.1',
sourceIp: '127.0.0.1'
}
} as any,
resource: handlerPath || '/',
stageVariables: null,
// Function URL format
rawPath: handlerPath || '/',
rawQueryString: fullUrl.search.slice(1) || ''
};

const result = await handlerFn(event);
res.statusCode = result.statusCode || 200;
if (result.headers) {
for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, String(v));
}
res.end(result.body);
});
} catch (err) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Server error' }));
}
});

server.listen(SHARED_SERVER_PORT, () => {
console.log(`Lambda Dev Server started on http://localhost:${SHARED_SERVER_PORT}`);
console.log(`Available handlers will be listed at http://localhost:${SHARED_SERVER_PORT}`);

// Register this handler
handlers.set(HANDLER_NAME, {
handler,
swaggerUtils: { loadOpenApiSpec, getSwaggerHtml },
handlerPath: __dirname
});

console.log(`Handler '${HANDLER_NAME}' available at: http://localhost:${SHARED_SERVER_PORT}${BASE_PATH}`);
console.log(`Swagger UI: http://localhost:${SHARED_SERVER_PORT}${BASE_PATH}/swagger`);
});
}

startOrJoinServer().catch(console.error);
Loading