Files
gh-jezweb-claude-skills-ski…/templates/kv-metadata-pattern.ts
2025-11-30 08:24:21 +08:00

569 lines
12 KiB
TypeScript

/**
* Cloudflare Workers KV - Metadata Patterns
*
* This template demonstrates:
* - Storing data in metadata for list() efficiency
* - Metadata-based filtering
* - Versioning with metadata
* - Audit trails
* - Feature flags with metadata
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Metadata Optimization Pattern
// ============================================================================
/**
* Store small values in metadata to avoid separate get() calls
* Maximum metadata size: 1024 bytes (JSON serialized)
*/
// ❌ BAD: Requires 2 operations per key
async function getStatusBad(kv: KVNamespace, userId: string) {
const status = await kv.get(`user:${userId}:status`);
const lastSeen = await kv.get(`user:${userId}:lastseen`);
return { status, lastSeen };
}
// ✅ GOOD: Single list() operation gets metadata for all users
async function getStatusGood(kv: KVNamespace) {
const users = await kv.list({ prefix: 'user:' });
return users.keys.map((key) => ({
userId: key.name.split(':')[1],
status: key.metadata?.status,
lastSeen: key.metadata?.lastSeen,
}));
}
// Example: User status with metadata
app.post('/users/:id/status', async (c) => {
const userId = c.req.param('id');
const { status } = await c.req.json<{ status: string }>();
try {
// Store empty value, all data in metadata
await c.env.MY_KV.put(`user:${userId}`, '', {
metadata: {
status,
lastSeen: Date.now(),
plan: 'free',
},
});
return c.json({
success: true,
message: 'Status updated',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all users with status (no additional get() calls needed!)
app.get('/users/status', async (c) => {
try {
const users = await c.env.MY_KV.list({ prefix: 'user:' });
const statuses = users.keys.map((key) => ({
userId: key.name.split(':')[1],
status: key.metadata?.status || 'unknown',
lastSeen: key.metadata?.lastSeen,
plan: key.metadata?.plan,
}));
return c.json({
success: true,
users: statuses,
count: statuses.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Versioning with Metadata
// ============================================================================
interface VersionedData {
content: any;
version: number;
updatedAt: number;
updatedBy: string;
}
// Write with versioning
app.put('/config/:key', async (c) => {
const key = c.req.param('key');
const content = await c.req.json();
const updatedBy = c.req.header('X-User-ID') || 'system';
try {
// Get current version
const existing = await c.env.MY_KV.getWithMetadata<
VersionedData,
{ version: number }
>(`config:${key}`, { type: 'json' });
const currentVersion = existing.metadata?.version || 0;
const newVersion = currentVersion + 1;
// Store new version
const data: VersionedData = {
content,
version: newVersion,
updatedAt: Date.now(),
updatedBy,
};
await c.env.MY_KV.put(`config:${key}`, JSON.stringify(data), {
metadata: {
version: newVersion,
updatedAt: data.updatedAt,
updatedBy,
},
});
// Store version history (optional)
await c.env.MY_KV.put(
`config:${key}:v${newVersion}`,
JSON.stringify(data),
{
expirationTtl: 86400 * 30, // Keep versions for 30 days
}
);
return c.json({
success: true,
version: newVersion,
previousVersion: currentVersion,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get config with version info
app.get('/config/:key', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
VersionedData,
{ version: number; updatedAt: number; updatedBy: string }
>(`config:${key}`, { type: 'json' });
if (!value) {
return c.json(
{
success: false,
error: 'Config not found',
},
404
);
}
return c.json({
success: true,
data: value,
metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get specific version
app.get('/config/:key/version/:version', async (c) => {
const key = c.req.param('key');
const version = c.req.param('version');
try {
const data = await c.env.MY_KV.get<VersionedData>(
`config:${key}:v${version}`,
{ type: 'json' }
);
if (!data) {
return c.json(
{
success: false,
error: `Version ${version} not found`,
},
404
);
}
return c.json({
success: true,
data,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Audit Trail with Metadata
// ============================================================================
interface AuditMetadata {
createdBy: string;
createdAt: number;
updatedBy: string;
updatedAt: number;
accessCount: number;
}
// Write with audit trail
app.post('/data/:key', async (c) => {
const key = c.req.param('key');
const value = await c.req.text();
const userId = c.req.header('X-User-ID') || 'anonymous';
try {
// Check if key exists
const existing = await c.env.MY_KV.getWithMetadata<string, AuditMetadata>(
key
);
const metadata: AuditMetadata = existing.metadata
? {
...existing.metadata,
updatedBy: userId,
updatedAt: Date.now(),
}
: {
createdBy: userId,
createdAt: Date.now(),
updatedBy: userId,
updatedAt: Date.now(),
accessCount: 0,
};
await c.env.MY_KV.put(key, value, { metadata });
return c.json({
success: true,
audit: metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with access tracking
app.get('/data/:key', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
string,
AuditMetadata
>(key);
if (!value) {
return c.json(
{
success: false,
error: 'Key not found',
},
404
);
}
// Increment access count (fire-and-forget)
if (metadata) {
c.executionCtx.waitUntil(
c.env.MY_KV.put(key, value, {
metadata: {
...metadata,
accessCount: metadata.accessCount + 1,
},
})
);
}
return c.json({
success: true,
value,
audit: metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Feature Flags with Metadata
// ============================================================================
interface FeatureFlag {
enabled: boolean;
rolloutPercentage: number;
targetUsers?: string[];
metadata: {
createdAt: number;
updatedAt: number;
description: string;
};
}
// Create feature flag
app.post('/flags/:name', async (c) => {
const name = c.req.param('name');
const flag = await c.req.json<FeatureFlag>();
try {
await c.env.MY_KV.put(`flag:${name}`, JSON.stringify(flag), {
metadata: {
enabled: flag.enabled,
rolloutPercentage: flag.rolloutPercentage,
updatedAt: Date.now(),
},
});
return c.json({
success: true,
message: `Feature flag "${name}" created`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all feature flags (metadata only)
app.get('/flags', async (c) => {
try {
const flags = await c.env.MY_KV.list({ prefix: 'flag:' });
const flagList = flags.keys.map((key) => ({
name: key.name.replace('flag:', ''),
enabled: key.metadata?.enabled || false,
rolloutPercentage: key.metadata?.rolloutPercentage || 0,
updatedAt: key.metadata?.updatedAt,
}));
return c.json({
success: true,
flags: flagList,
count: flagList.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Check feature flag for user
app.get('/flags/:name/check/:userId', async (c) => {
const name = c.req.param('name');
const userId = c.req.param('userId');
try {
const flag = await c.env.MY_KV.get<FeatureFlag>(`flag:${name}`, {
type: 'json',
});
if (!flag) {
return c.json(
{
success: false,
error: 'Feature flag not found',
},
404
);
}
// Check if enabled
if (!flag.enabled) {
return c.json({ enabled: false, reason: 'Flag disabled globally' });
}
// Check target users
if (flag.targetUsers && flag.targetUsers.length > 0) {
const enabled = flag.targetUsers.includes(userId);
return c.json({
enabled,
reason: enabled ? 'User in target list' : 'User not in target list',
});
}
// Check rollout percentage
const userHash =
parseInt(userId.split('').reduce((a, b) => a + b.charCodeAt(0), 0).toString()) %
100;
const enabled = userHash < flag.rolloutPercentage;
return c.json({
enabled,
reason: enabled
? `User in ${flag.rolloutPercentage}% rollout`
: `User not in ${flag.rolloutPercentage}% rollout`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Metadata Size Validation
// ============================================================================
// Validate metadata size before writing
app.post('/validate/metadata', async (c) => {
const { metadata } = await c.req.json<{ metadata: any }>();
const serialized = JSON.stringify(metadata);
const size = new TextEncoder().encode(serialized).length;
if (size > 1024) {
return c.json({
valid: false,
size,
maxSize: 1024,
error: `Metadata too large: ${size} bytes (max 1024)`,
});
}
return c.json({
valid: true,
size,
maxSize: 1024,
});
});
// ============================================================================
// Metadata Migration
// ============================================================================
// Migrate existing keys to add metadata
app.post('/migrate/add-metadata', async (c) => {
const { prefix, metadata } = await c.req.json<{
prefix: string;
metadata: any;
}>();
try {
let migratedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ prefix, cursor });
// Migrate batch
for (const key of result.keys) {
// Get existing value
const value = await c.env.MY_KV.get(key.name);
if (value !== null) {
// Re-write with metadata
await c.env.MY_KV.put(key.name, value, {
metadata: {
...key.metadata,
...metadata,
migratedAt: Date.now(),
},
});
migratedCount++;
}
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Migrated ${migratedCount} keys`,
migratedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;