Files
gh-jezweb-claude-skills-ski…/templates/context-extension.ts
2025-11-30 08:24:59 +08:00

423 lines
10 KiB
TypeScript

/**
* Hono Context Extension
*
* Type-safe context extension using c.set() and c.get() with custom Variables.
*/
import { Hono } from 'hono'
import type { Context, Next } from 'hono'
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
// Define environment bindings (for Cloudflare Workers, etc.)
type Bindings = {
DATABASE_URL: string
API_KEY: string
ENVIRONMENT: 'development' | 'staging' | 'production'
}
// Define context variables (c.set/c.get)
type Variables = {
user: {
id: string
email: string
name: string
role: 'admin' | 'user'
}
requestId: string
startTime: number
logger: {
info: (message: string, meta?: any) => void
warn: (message: string, meta?: any) => void
error: (message: string, meta?: any) => void
}
db: {
query: <T>(sql: string, params?: any[]) => Promise<T[]>
execute: (sql: string, params?: any[]) => Promise<void>
}
cache: {
get: (key: string) => Promise<string | null>
set: (key: string, value: string, ttl?: number) => Promise<void>
delete: (key: string) => Promise<void>
}
}
// Create typed app
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// ============================================================================
// REQUEST ID MIDDLEWARE
// ============================================================================
app.use('*', async (c, next) => {
const requestId = crypto.randomUUID()
c.set('requestId', requestId)
await next()
c.res.headers.set('X-Request-ID', requestId)
})
// ============================================================================
// PERFORMANCE TIMING MIDDLEWARE
// ============================================================================
app.use('*', async (c, next) => {
const startTime = Date.now()
c.set('startTime', startTime)
await next()
const elapsed = Date.now() - startTime
c.res.headers.set('X-Response-Time', `${elapsed}ms`)
const logger = c.get('logger')
logger.info(`Request completed in ${elapsed}ms`, {
path: c.req.path,
method: c.req.method,
})
})
// ============================================================================
// LOGGER MIDDLEWARE
// ============================================================================
app.use('*', async (c, next) => {
const requestId = c.get('requestId')
const logger = {
info: (message: string, meta?: any) => {
console.log(
JSON.stringify({
level: 'info',
requestId,
message,
...meta,
timestamp: new Date().toISOString(),
})
)
},
warn: (message: string, meta?: any) => {
console.warn(
JSON.stringify({
level: 'warn',
requestId,
message,
...meta,
timestamp: new Date().toISOString(),
})
)
},
error: (message: string, meta?: any) => {
console.error(
JSON.stringify({
level: 'error',
requestId,
message,
...meta,
timestamp: new Date().toISOString(),
})
)
},
}
c.set('logger', logger)
await next()
})
// ============================================================================
// DATABASE MIDDLEWARE
// ============================================================================
app.use('/api/*', async (c, next) => {
// Simulated database connection
const db = {
query: async <T>(sql: string, params?: any[]): Promise<T[]> => {
const logger = c.get('logger')
logger.info('Executing query', { sql, params })
// Simulated query execution
return [] as T[]
},
execute: async (sql: string, params?: any[]): Promise<void> => {
const logger = c.get('logger')
logger.info('Executing statement', { sql, params })
// Simulated execution
},
}
c.set('db', db)
await next()
})
// ============================================================================
// CACHE MIDDLEWARE
// ============================================================================
app.use('/api/*', async (c, next) => {
// Simulated cache (use Redis, KV, etc. in production)
const cacheStore = new Map<string, { value: string; expiresAt: number }>()
const cache = {
get: async (key: string): Promise<string | null> => {
const logger = c.get('logger')
const entry = cacheStore.get(key)
if (!entry) {
logger.info('Cache miss', { key })
return null
}
if (entry.expiresAt < Date.now()) {
logger.info('Cache expired', { key })
cacheStore.delete(key)
return null
}
logger.info('Cache hit', { key })
return entry.value
},
set: async (key: string, value: string, ttl: number = 60000): Promise<void> => {
const logger = c.get('logger')
logger.info('Cache set', { key, ttl })
cacheStore.set(key, {
value,
expiresAt: Date.now() + ttl,
})
},
delete: async (key: string): Promise<void> => {
const logger = c.get('logger')
logger.info('Cache delete', { key })
cacheStore.delete(key)
},
}
c.set('cache', cache)
await next()
})
// ============================================================================
// AUTHENTICATION MIDDLEWARE
// ============================================================================
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
const logger = c.get('logger')
if (!token) {
logger.warn('Missing authentication token')
return c.json({ error: 'Unauthorized' }, 401)
}
// Simulated token validation
if (token !== 'valid-token') {
logger.warn('Invalid authentication token')
return c.json({ error: 'Invalid token' }, 401)
}
// Simulated user lookup
const user = {
id: '123',
email: 'user@example.com',
name: 'John Doe',
role: 'user' as const,
}
c.set('user', user)
logger.info('User authenticated', { userId: user.id })
await next()
})
// ============================================================================
// ROUTES USING CONTEXT
// ============================================================================
// Route using logger
app.get('/api/log-example', (c) => {
const logger = c.get('logger')
logger.info('This is an info message')
logger.warn('This is a warning')
logger.error('This is an error')
return c.json({ message: 'Logged' })
})
// Route using user
app.get('/api/profile', (c) => {
const user = c.get('user')
return c.json({
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
})
})
// Route using database
app.get('/api/users', async (c) => {
const db = c.get('db')
const logger = c.get('logger')
try {
const users = await db.query<{ id: string; name: string }>('SELECT * FROM users')
return c.json({ users })
} catch (error) {
logger.error('Database query failed', { error })
return c.json({ error: 'Database error' }, 500)
}
})
// Route using cache
app.get('/api/cached-data', async (c) => {
const cache = c.get('cache')
const logger = c.get('logger')
const cacheKey = 'expensive-data'
// Try to get from cache
const cached = await cache.get(cacheKey)
if (cached) {
return c.json({ data: JSON.parse(cached), cached: true })
}
// Simulate expensive computation
const data = { result: 'expensive data', timestamp: Date.now() }
// Store in cache
await cache.set(cacheKey, JSON.stringify(data), 60000) // 1 minute
return c.json({ data, cached: false })
})
// Route using request ID
app.get('/api/request-info', (c) => {
const requestId = c.get('requestId')
const startTime = c.get('startTime')
const elapsed = Date.now() - startTime
return c.json({
requestId,
elapsed: `${elapsed}ms`,
method: c.req.method,
path: c.req.path,
})
})
// Route using environment bindings
app.get('/api/env', (c) => {
const environment = c.env.ENVIRONMENT
const apiKey = c.env.API_KEY // Don't expose this in real app!
return c.json({
environment,
hasApiKey: !!apiKey,
})
})
// ============================================================================
// COMBINING MULTIPLE CONTEXT VALUES
// ============================================================================
app.post('/api/create-user', async (c) => {
const logger = c.get('logger')
const db = c.get('db')
const user = c.get('user')
const requestId = c.get('requestId')
// Check permissions
if (user.role !== 'admin') {
logger.warn('Unauthorized user creation attempt', {
userId: user.id,
requestId,
})
return c.json({ error: 'Forbidden' }, 403)
}
// Parse request body
const body = await c.req.json()
// Create user
try {
await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', [body.name, body.email])
logger.info('User created', {
createdBy: user.id,
newUserEmail: body.email,
requestId,
})
return c.json({ success: true }, 201)
} catch (error) {
logger.error('User creation failed', {
error,
requestId,
})
return c.json({ error: 'Failed to create user' }, 500)
}
})
// ============================================================================
// CUSTOM CONTEXT HELPERS
// ============================================================================
// Helper to get authenticated user (with type guard)
function getAuthenticatedUser(c: Context<{ Bindings: Bindings; Variables: Variables }>) {
const user = c.get('user')
if (!user) {
throw new Error('User not authenticated')
}
return user
}
// Helper to check admin role
function requireAdmin(c: Context<{ Bindings: Bindings; Variables: Variables }>) {
const user = getAuthenticatedUser(c)
if (user.role !== 'admin') {
throw new Error('Admin access required')
}
return user
}
// Usage
app.delete('/api/users/:id', (c) => {
const admin = requireAdmin(c) // Throws if not admin
const logger = c.get('logger')
logger.info('User deletion requested', {
adminId: admin.id,
targetUserId: c.req.param('id'),
})
return c.json({ success: true })
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
export type { Bindings, Variables }
export { getAuthenticatedUser, requireAdmin }