Savvi Studio

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`);
});

These patterns demonstrate real-world usage of generated functions. Adapt them to your specific needs.