Function Wrapper Patterns
Purpose: Detailed examples and patterns for using generated functions
Last Updated: 2024-11-28
This document contains comprehensive examples extracted from the main function wrappers documentation. For core concepts, see Function Wrappers.
Common Patterns
Transaction Pattern
import { withTransaction } from '@/lib/db';
import { createResource, linkResources } from '@db/graph';
await withTransaction(async (client) => {
const id1 = await createResource(client, {
p_type_namespace: 'test.user',
p_data: {}
});
const id2 = await createResource(client, {
p_type_namespace: 'test.post',
p_data: {}
});
await linkResources(client, {
p_from_id: id1,
p_to_id: id2,
p_edge_type: 'authored'
});
});
Error Handling Pattern
import { ZodError } from 'zod';
import { DatabaseError } from 'pg';
try {
await createResource(client, params);
} catch (error) {
if (error instanceof ZodError) {
// Validation error - user provided invalid data
console.error('Invalid input:', error.errors);
// Return 400 Bad Request
} else if (error instanceof DatabaseError) {
// Database error - constraint violation, connection issues, etc.
console.error('Database error:', error.message, error.code);
// Handle specific error codes
if (error.code === '23505') {
// Unique constraint violation
console.error('Resource already exists');
}
} else {
// Unknown error - rethrow
throw error;
}
}
Batch Operations Pattern
import { withClient } from '@/lib/db';
import { createResource } from '@db/graph';
// Parallel execution (independent operations)
const results = await withClient(async (client) => {
return await Promise.all([
createResource(client, { p_type_namespace: 'test.user', p_data: {} }),
createResource(client, { p_type_namespace: 'test.post', p_data: {} }),
createResource(client, { p_type_namespace: 'test.comment', p_data: {} })
]);
});
// Sequential execution (dependent operations)
const result = await withClient(async (client) => {
const userId = await createResource(client, {
p_type_namespace: 'test.user',
p_data: { name: 'John' }
});
const postId = await createResource(client, {
p_type_namespace: 'test.post',
p_data: { author_id: userId, title: 'Hello' }
});
return { userId, postId };
});
Conditional Logic Pattern
import { withClient } from '@/lib/db';
import { findUser, createUser } from '@db/auth';
// Find or create pattern
const user = await withClient(async (client) => {
const existing = await findUser(client, { p_email: email });
if (existing) {
return existing;
}
return await createUser(client, {
p_email: email,
p_name: name
});
});
// Update or create pattern
const user = await withClient(async (client) => {
try {
return await updateUser(client, { p_id: userId, p_name: newName });
} catch (error) {
if (error instanceof DatabaseError && error.code === '23503') {
// Foreign key violation - user doesn't exist
return await createUser(client, { p_email: email, p_name: newName });
}
throw error;
}
});
Testing Patterns
Mock Client Pattern
import { describe, it, expect, vi } from 'vitest';
import { createResource } from '@db/graph';
describe('createResource', () => {
it('creates resource with valid params', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
rows: [{ create_resource: 123n }]
})
} as any;
const result = await createResource(mockClient, {
p_type_namespace: 'test.user',
p_data: { name: 'John' }
});
expect(result).toBe(123n);
expect(mockClient.query).toHaveBeenCalledWith(
expect.stringContaining('create_resource'),
expect.arrayContaining(['test.user'])
);
});
it('validates input parameters', async () => {
const mockClient = { query: vi.fn() } as any;
await expect(
createResource(mockClient, {
p_type_namespace: 123 as any, // Wrong type
p_data: {}
})
).rejects.toThrow();
});
it('handles database errors', async () => {
const mockClient = {
query: vi.fn().mockRejectedValue(new Error('Connection failed'))
} as any;
await expect(
createResource(mockClient, {
p_type_namespace: 'test.user',
p_data: {}
})
).rejects.toThrow('Connection failed');
});
});
Integration Test Pattern
import { withTestClient } from '@/test/utils';
import { createResource, getResource, deleteResource } from '@db/graph';
describe('resource operations', () => {
it('creates and retrieves resource', async () => {
await withTestClient(async (client) => {
// Create
const id = await createResource(client, {
p_type_namespace: 'test.user',
p_external_id: 'user-123',
p_data: { name: 'John', age: 30 }
});
expect(id).toBeGreaterThan(0n);
// Retrieve
const resource = await getResource(client, { p_id: id });
expect(resource.id).toBe(id);
expect(resource.external_id).toBe('user-123');
expect(resource.data).toEqual({ name: 'John', age: 30 });
});
});
it('handles concurrent operations', async () => {
await withTestClient(async (client) => {
// Create multiple resources in parallel
const ids = await Promise.all([
createResource(client, { p_type_namespace: 'test.user', p_data: { name: 'User1' } }),
createResource(client, { p_type_namespace: 'test.user', p_data: { name: 'User2' } }),
createResource(client, { p_type_namespace: 'test.user', p_data: { name: 'User3' } })
]);
expect(ids).toHaveLength(3);
expect(new Set(ids).size).toBe(3); // All unique
});
});
it('enforces constraints', async () => {
await withTestClient(async (client) => {
// Create with unique constraint
await createResource(client, {
p_type_namespace: 'test.user',
p_external_id: 'unique-123',
p_data: {}
});
// Try to create duplicate
await expect(
createResource(client, {
p_type_namespace: 'test.user',
p_external_id: 'unique-123',
p_data: {}
})
).rejects.toThrow();
});
});
});
Advanced Patterns
Type Guard Pattern
interface UserResource {
id: bigint;
type_namespace: string;
data: {
name: string;
email: string;
};
}
interface PostResource {
id: bigint;
type_namespace: string;
data: {
title: string;
content: string;
};
}
function isUser(resource: Resource): resource is UserResource {
return resource.type_namespace.startsWith('test.user');
}
function isPost(resource: Resource): resource is PostResource {
return resource.type_namespace.startsWith('test.post');
}
// Usage
const resource = await getResource(client, { p_id: id });
if (isUser(resource)) {
// TypeScript knows resource is UserResource
console.log(resource.data.email);
} else if (isPost(resource)) {
// TypeScript knows resource is PostResource
console.log(resource.data.title);
}
Generic Function Pattern
async function findOrCreate<T extends Resource>(
client: PoolClient,
finder: () => Promise<T | null>,
creator: () => Promise<T>
): Promise<T> {
const existing = await finder();
return existing ?? await creator();
}
// Usage with different resource types
const user = await findOrCreate(
client,
() => findUser(client, { p_email: 'user@example.com' }),
() => createUser(client, { p_email: 'user@example.com', p_name: 'John' })
);
const post = await findOrCreate(
client,
() => findPost(client, { p_slug: 'hello-world' }),
() => createPost(client, { p_slug: 'hello-world', p_title: 'Hello World' })
);
Retry Wrapper Pattern
async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number;
delayMs?: number;
backoff?: 'linear' | 'exponential';
} = {}
): Promise<T> {
const {
maxAttempts = 3,
delayMs = 100,
backoff = 'exponential'
} = options;
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === maxAttempts) {
throw lastError;
}
// Calculate delay
const delay = backoff === 'exponential'
? delayMs * Math.pow(2, attempt - 1)
: delayMs * attempt;
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage
const resource = await withRetry(
() => withClient(client => createResource(client, params)),
{ maxAttempts: 5, delayMs: 200, backoff: 'exponential' }
);
Pagination Pattern
import { listResources } from '@db/graph';
async function* paginateResources(
client: PoolClient,
typeNamespace: string,
pageSize: number = 100
) {
let cursor: string | null = null;
while (true) {
const page = await listResources(client, {
p_type_namespace: typeNamespace,
p_cursor: cursor,
p_limit: pageSize
});
if (page.items.length === 0) {
break;
}
yield page.items;
if (!page.has_more) {
break;
}
cursor = page.next_cursor;
}
}
// Usage
for await (const batch of paginateResources(client, 'test.user', 50)) {
console.log(`Processing ${batch.length} users`);
await processBatch(batch);
}
Caching Pattern
class ResourceCache {
private cache = new Map<bigint, Resource>();
private ttl = 60000; // 1 minute
async get(
client: PoolClient,
id: bigint
): Promise<Resource> {
const cached = this.cache.get(id);
if (cached && this.isValid(cached)) {
return cached;
}
const fresh = await getResource(client, { p_id: id });
this.cache.set(id, { ...fresh, _cached_at: Date.now() });
return fresh;
}
private isValid(resource: Resource & { _cached_at?: number }): boolean {
if (!resource._cached_at) return false;
return Date.now() - resource._cached_at < this.ttl;
}
invalidate(id: bigint): void {
this.cache.delete(id);
}
clear(): void {
this.cache.clear();
}
}
// Usage
const cache = new ResourceCache();
const resource1 = await cache.get(client, 123n); // Database query
const resource2 = await cache.get(client, 123n); // From cache
// After update
await updateResource(client, { p_id: 123n, p_data: newData });
cache.invalidate(123n);
Performance Patterns
Connection Pooling Best Practices
import { withClient, withTransaction } from '@/lib/db';
// ✅ Good - automatic connection management
await withClient(async (client) => {
return await createResource(client, params);
});
// ✅ Good - transaction handles connection
await withTransaction(async (client) => {
await createResource(client, params1);
await createResource(client, params2);
});
// ❌ Bad - manual connection management
const client = await pool.connect();
try {
return await createResource(client, params);
} finally {
client.release(); // Easy to forget
}
Parallel vs Sequential
// Parallel - for independent operations
const [users, posts, comments] = await withClient(async (client) => {
return await Promise.all([
listUsers(client),
listPosts(client),
listComments(client)
]);
});
// Sequential - for dependent operations
const result = await withClient(async (client) => {
const user = await createUser(client, userParams);
const post = await createPost(client, { ...postParams, p_user_id: user.id });
const comment = await createComment(client, { ...commentParams, p_post_id: post.id });
return { user, post, comment };
});
Batch Insert Pattern
async function batchInsertResources(
client: PoolClient,
resources: Array<{ type_namespace: string; data: unknown }>,
batchSize: number = 100
): Promise<bigint[]> {
const ids: bigint[] = [];
for (let i = 0; i < resources.length; i += batchSize) {
const batch = resources.slice(i, i + batchSize);
const batchIds = await Promise.all(
batch.map(r => createResource(client, {
p_type_namespace: r.type_namespace,
p_data: r.data
}))
);
ids.push(...batchIds);
}
return ids;
}
// Usage
await withTransaction(async (client) => {
const resources = Array.from({ length: 1000 }, (_, i) => ({
type_namespace: 'test.user',
data: { name: `User ${i}` }
}));
const ids = await batchInsertResources(client, resources);
console.log(`Created ${ids.length} resources`);
});
Related Documentation
- Function Wrappers - Core concepts
- Quick Reference - Common tasks
- Type System - Type mappings
- Database Best Practices - General patterns
These patterns demonstrate real-world usage of generated functions. Adapt them to your specific needs.