Skip to content
Open
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
26 changes: 23 additions & 3 deletions api-expressjs-vm/index-logging.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const express = require('express')
const app = express()
const os = require('os');
const crypto = require('crypto');

// console.log(JSON.stringify(process.env));

Expand All @@ -26,17 +27,36 @@ AppInsights.setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY)

const AppInsightsClient = AppInsights.defaultClient;

// Correlation ID + request duration middleware
app.use((req, res, next) => {
const id = req.headers['x-correlation-id'];
req.correlationId = Array.isArray(id) ? id[0] : id || crypto.randomUUID();
req.startTime = Date.now();
res.setHeader('x-correlation-id', req.correlationId);
res.on('finish', () => {
const durationMs = Date.now() - req.startTime;
console.log(JSON.stringify({
method: req.method,
path: req.originalUrl,
statusCode: res.statusCode,
durationMs,
correlationId: req.correlationId
}));
});
next();
});


app.get('/trace', (req, res) => {

const clientIP = req.headers['x-forwarded-for'];
const msg = `trace route ${os.hostname()} ${clientIP} ${new Date()}`;

console.log(msg)
console.log(`[${req.correlationId}] ${msg}`)

if (process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
AppInsightsClient.trackPageView();
AppInsightsClient.trackTrace({ message: msg })
AppInsightsClient.trackTrace({ message: `[${req.correlationId}] ${msg}` })
AppInsightsClient.flush();
} else {
msg += ' AppInsights not configured';
Expand All @@ -50,7 +70,7 @@ app.get('/', function (req, res) {
const clientIP = req.headers['x-forwarded-for'];
const msg = `root route ${os.hostname()} ${clientIP} ${new Date()}`

console.log(msg)
console.log(`[${req.correlationId}] ${msg}`)

res.send(msg)

Expand Down
28 changes: 24 additions & 4 deletions api-expressjs-vm/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
const os = require('os');
const express = require('express')
const app = express()
const crypto = require('crypto');
const express = require('express');
const app = express();

// Correlation ID + request duration middleware
app.use((req, res, next) => {
const id = req.headers['x-correlation-id'];
req.correlationId = Array.isArray(id) ? id[0] : id || crypto.randomUUID();
req.startTime = Date.now();
res.setHeader('x-correlation-id', req.correlationId);
res.on('finish', () => {
const durationMs = Date.now() - req.startTime;
console.log(JSON.stringify({
method: req.method,
path: req.originalUrl,
statusCode: res.statusCode,
durationMs,
correlationId: req.correlationId
}));
});
next();
});

app.use('/public', express.static('public'))
app.get('/', function (req, res) {

const clientIP = req.headers['x-forwarded-for'];
const msg = `HostName: ${os.hostname()}<br>ClientIP: ${clientIP}<br>DateTime: ${new Date()}<br><img width='200' height='200' src='/public/leaves.jpg' alt='flowers'>`
console.log(msg)
const msg = `HostName: ${os.hostname()}<br>ClientIP: ${clientIP}<br>DateTime: ${new Date()}<br><img width='200' height='200' src='/public/leaves.jpg' alt='flowers'>`;
console.log(`[${req.correlationId}] ${msg}`);

res.send(msg)
})
Expand Down
182 changes: 133 additions & 49 deletions api-inmemory/src/functions/todo.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,154 @@
// @ts-nocheck

//create a todo, view a todo, modify a todo, list all todos, delete all todos

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
// import { MongoClient } from 'mongodb';
import { TodoItem, BulkCreateResponse, ValidationError } from "../models";
import * as todoService from "../services/todoService";

let nextId = 2;
// ── helpers ──────────────────────────────────────────────

type Todo = {
title: string;
/** Read or generate a correlation ID from the incoming request. */
function correlationId(request: HttpRequest): string {
return request.headers.get("x-correlation-id") ?? crypto.randomUUID();
}

let todos = {
"1": "Say hello"
};

// ── GET /api/todo ────────────────────────────────────────

app.get('getAll', {
app.get("getAll", {
route: "todo",
authLevel: 'anonymous',
handler: async function getAll(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
authLevel: "anonymous",
handler: async function getAll(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
return { jsonBody: todoService.getAll() };
},
});

// ── POST /api/todo (single create) ─────────────────────

app.post("addOne", {
route: "todo",
authLevel: "anonymous",
handler: async function addOne(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
const body = (await request.json()) as Record<string, unknown>;
context.log("addOne body", body);

if (body && typeof body.title === "string" && body.title.trim().length > 0) {
const id = todoService.addOne(body.title.trim());
return { jsonBody: { id, title: body.title.trim() } };
}

return { jsonBody: todos }
}
})
return {
status: 400,
jsonBody: { error: "Request body must include a non-empty 'title' string." },
};
},
});

// ── POST /api/todo/bulk (bulk create) ──────────────────

app.post("bulkCreate", {
route: "todo/bulk",
authLevel: "anonymous",
handler: async function bulkCreate(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
const cid = correlationId(request);

// ── Parse body ────────────────────────────────────
let body: unknown;
try {
body = await request.json();
} catch {
return {
status: 400,
headers: { "x-correlation-id": cid },
jsonBody: { correlationId: cid, error: "Request body must be valid JSON." },
};
}

// curl --location 'http://localhost:7071/api/users?user=mike' \
// --header 'Content-Type: application/json' \
// --data '{
// "name": "dina",
// "age": "21"
// }'
// ── Validate top-level shape ──────────────────────
if (!body || !Array.isArray((body as any).items)) {
return {
status: 400,
headers: { "x-correlation-id": cid },
jsonBody: {
correlationId: cid,
error: "Request body must be an object with an 'items' array. Example: { \"items\": [{ \"title\": \"...\" }] }",
},
};
}

app.post('addOne', {
route: "todo",
authLevel: 'anonymous',
handler: async function addOne(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const items: unknown[] = (body as any).items;

const body = await request.json();
console.log(body)
if (items.length === 0) {
return {
status: 400,
headers: { "x-correlation-id": cid },
jsonBody: { correlationId: cid, error: "'items' array must not be empty." },
};
}

if(body && body.title){
todos[nextId++] = body.title;
// ── Per-item pre-validation ───────────────────────
const errors: ValidationError[] = [];
const validItems: TodoItem[] = [];

console.log(todos);

return {
jsonBody: body.title
for (let i = 0; i < items.length; i++) {
const item = items[i] as Record<string, unknown>;
if (!item || typeof item !== "object" || typeof item.title !== "string" || (item.title as string).trim().length === 0) {
errors.push({ index: i, message: "Each item must be an object with a non-empty 'title' string." });
} else {
validItems.push({ title: (item.title as string).trim() });
}
}
return { status: 404 }

}
})
if (errors.length > 0) {
return {
status: 400,
headers: { "x-correlation-id": cid },
jsonBody: {
correlationId: cid,
error: "Validation failed for one or more items.",
validationErrors: errors,
},
};
}

// ── Batched insert via service ────────────────────
const results = todoService.bulkCreate(validItems);
const succeeded = results.filter((r) => r.success).length;
const failed = results.length - succeeded;

app.deleteRequest('deleteAll', {
route: "todo",
authLevel: 'anonymous',
handler: async function deleteAll(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`[${cid}] bulkCreate: ${succeeded} succeeded, ${failed} failed out of ${results.length}`);

todos = {};
nextId=1;
const response: BulkCreateResponse = {
correlationId: cid,
total: results.length,
succeeded,
failed,
results,
};

return {
jsonBody: todos
}
}
})
status: failed > 0 ? 207 : 201,
headers: { "x-correlation-id": cid },
jsonBody: response,
};
},
});

// ── DELETE /api/todo ─────────────────────────────────────

app.deleteRequest("deleteAll", {
route: "todo",
authLevel: "anonymous",
handler: async function deleteAll(
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
todoService.deleteAll();
return { jsonBody: todoService.getAll() };
},
});
24 changes: 24 additions & 0 deletions api-inmemory/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface TodoItem {
title: string;
}

export interface BulkItemResult {
index: number;
success: boolean;
id?: string;
title?: string;
error?: string;
}

export interface BulkCreateResponse {
correlationId: string;
total: number;
succeeded: number;
failed: number;
results: BulkItemResult[];
}

export interface ValidationError {
index: number;
message: string;
}
43 changes: 43 additions & 0 deletions api-inmemory/src/services/todoService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TodoItem, BulkItemResult } from "../models";

// ── In-memory store ──────────────────────────────────────
let nextId = 2;
let todos: Record<string, string> = { "1": "Say hello" };

// ── Single-item operations ───────────────────────────────
export function getAll(): Record<string, string> {
return todos;
}

export function addOne(title: string): string {
const id = String(nextId++);
todos[id] = title;
return id;
}

export function deleteAll(): void {
todos = {};
nextId = 1;
}

// ── Batched bulk insert ──────────────────────────────────
// Performs a single pass over the array — no per-item async work.
export function bulkCreate(items: TodoItem[]): BulkItemResult[] {
const results: BulkItemResult[] = [];

for (let i = 0; i < items.length; i++) {
const item = items[i];
try {
if (!item.title || typeof item.title !== "string" || item.title.trim().length === 0) {
results.push({ index: i, success: false, error: "title must be a non-empty string" });
continue;
}
const id = addOne(item.title.trim());
results.push({ index: i, success: true, id, title: item.title.trim() });
} catch (err) {
results.push({ index: i, success: false, error: String(err) });
}
}

return results;
}
Loading