Skip to content

Column Name Transformation

Transform between JavaScript camelCase and PostgreSQL snake_case column names automatically with both PostgREST syntax and ORM-style repository patterns.

Overview

PGRestify provides automatic bidirectional column name transformation, similar to naming strategies in ORMs. This feature allows you to write JavaScript code using familiar camelCase naming conventions while maintaining PostgreSQL snake_case standards in your database.

Both PostgREST native syntax and repository patterns support full column transformation capabilities.

Key Benefits

  • Native JavaScript conventions - Use firstName instead of first_name
  • Database compatibility - Maintains PostgreSQL naming standards
  • Bidirectional transformation - Works for both queries and responses
  • Deep nesting support - Transforms nested objects and arrays recursively
  • Type safety - Full TypeScript support maintained
  • Zero breaking changes - Opt-in feature with backward compatibility

Quick Start

Basic Example

typescript
import { createClient } from '@webcoded/pgrestify';

// Enable transformation globally
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: true  // Default: false
});

// Use camelCase in your TypeScript interfaces
interface User {
  userId: number;
  firstName: string;
  lastName: string;
  emailAddress: string;
  isActive: boolean;
  createdAt: string;
}

// Write queries using camelCase
const users = await client
  .from<User>('users')
  .select('userId', 'firstName', 'lastName')
  .where('isActive', 'eq', true)
  .order('createdAt', { ascending: false })
  .execute();

// Response data has camelCase properties
console.log(users.data[0].firstName); // ✅ Works!
typescript
import { createClient } from '@webcoded/pgrestify';

// Enable transformation globally
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: true  // Default: false
});

// Use camelCase in your TypeScript interfaces
interface User {
  userId: number;
  firstName: string;
  lastName: string;
  emailAddress: string;
  isActive: boolean;
  createdAt: string;
}

// Get repository with transformation
const userRepo = client.getRepository<User>('users');

// Repository methods automatically use transformation
const users = await userRepo.find({
  select: ['userId', 'firstName', 'lastName'],
  where: { isActive: true },
  order: { createdAt: 'DESC' },
  take: 10
});

// Response data has camelCase properties
console.log(users[0].firstName); // ✅ Works!

// Alternative: Query builder with repository
const queryBuilderUsers = await userRepo
  .createQueryBuilder()
  .select(['userId', 'firstName', 'lastName'])
  .where('isActive = :active', { active: true })
  .orderBy('createdAt', 'DESC')
  .getMany();

console.log(queryBuilderUsers[0].firstName); // ✅ Works!

What Happens Under the Hood

typescript
// Your JavaScript code:
.select('userId', 'firstName', 'lastName')
.where('isActive', 'eq', true)

// Generated PostgREST query:
// GET /users?select=user_id,first_name,last_name&is_active=eq.true

// Database response (snake_case):
[{ user_id: 1, first_name: "John", last_name: "Doe" }]

// Transformed response (camelCase):
[{ userId: 1, firstName: "John", lastName: "Doe" }]

Configuration Options

Global Configuration

Enable transformation for all queries in your application:

typescript
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: true  // Apply to all queries
});

Query-Level Configuration

Override global settings for specific queries:

typescript
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: false  // Disabled globally
});

// Enable for specific queries
const query1 = client
  .from('users')
  .transformColumns(true)  // ✅ Enable transformation
  .select('firstName', 'lastName');

// Keep disabled for others
const query2 = client
  .from('users')
  .transformColumns(false) // ✅ Explicit disable
  .select('first_name', 'last_name');
typescript
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: false  // Disabled globally
});

const userRepo = client.getRepository<User>('users');

// Enable transformation for specific repository queries
const usersWithTransform = await userRepo.find({
  select: ['firstName', 'lastName'],
  transformColumns: true  // ✅ Enable transformation
});

// Keep disabled for specific queries
const usersWithoutTransform = await userRepo.find({
  select: ['first_name', 'last_name'],
  transformColumns: false // ✅ Explicit disable
});

// Query builder with transformation control
const transformedQuery = await userRepo
  .createQueryBuilder()
  .select(['firstName', 'lastName'])
  .transformColumns(true)  // ✅ Enable for this query
  .getMany();

const rawQuery = await userRepo
  .createQueryBuilder()
  .select(['first_name', 'last_name'])
  .transformColumns(false) // ✅ Keep disabled
  .getMany();

React Hook Configuration

Use transformation with React hooks:

typescript
import { useQuery, useInfiniteQuery } from '@webcoded/pgrestify/react';

// Modern useQuery with config object
const { data, isLoading } = useQuery<User>({
  from: 'users',
  select: ['userId', 'firstName', 'lastName'],
  filter: { isActive: true },
  transformColumns: true  // ✅ Enable transformation
});

// Infinite query with transformation
const { data, fetchNextPage } = useInfiniteQuery<User>('users', {
  select: ['userId', 'firstName'],
  transformColumns: true,
  pageSize: 10
});

Query Methods Support

All query methods support column transformation:

Find Methods

typescript
// findOne() with transformation
const user = await client
  .from('users')
  .transformColumns(true)
  .findOne({
    where: { userId: 1 },
    select: ['firstName', 'lastName', 'emailAddress']
  });

// findOneOrFail() with relations
const courseWithCategory = await client
  .from('course')
  .transformColumns(true)
  .findOneOrFail({
    where: { id: 'uuid-here' },
    relations: ['courseCategory', 'courseTranslation'],
    select: ['id', 'finalPrice', 'createdAt']
  });

// find() with complex queries
const courses = await client
  .from('course')
  .transformColumns(true)
  .find({
    where: { publishedAt: true },
    select: ['id', 'finalPrice', 'courseCategory.categoryName'],
    order: { createdAt: 'DESC' },
    take: 10
  });
typescript
// Repository findOne() with transformation
const userRepo = client.getRepository<User>('users');
const courseRepo = client.getRepository<Course>('course');

const user = await userRepo.findOne({
  where: { userId: 1 },
  select: ['firstName', 'lastName', 'emailAddress']
  // transformColumns inherited from client configuration
});

// findOneOrFail() with relations
const courseWithCategory = await courseRepo.findOneOrFail({
  where: { id: 'uuid-here' },
  relations: ['courseCategory', 'courseTranslation'],
  select: ['id', 'finalPrice', 'createdAt']
});

// find() with complex queries
const courses = await courseRepo.find({
  where: { publishedAt: true },
  select: ['id', 'finalPrice', 'courseCategory.categoryName'],
  order: { createdAt: 'DESC' },
  take: 10
});

// Query builder equivalents
const userWithBuilder = await userRepo
  .createQueryBuilder()
  .select(['firstName', 'lastName', 'emailAddress'])
  .where('userId = :id', { id: 1 })
  .getOne();

const coursesWithBuilder = await courseRepo
  .createQueryBuilder('c')
  .leftJoinAndSelect('c.courseCategory', 'category')
  .select(['c.id', 'c.finalPrice', 'category.categoryName'])
  .where('c.publishedAt = :published', { published: true })
  .orderBy('c.createdAt', 'DESC')
  .take(10)
  .getMany();

Direct Query Building

typescript
// Chainable query builder
const users = await client
  .from<User>('users')
  .transformColumns(true)
  .select('userId', 'firstName', 'lastName')
  .where('isActive', 'eq', true)
  .where('firstName', 'ilike', '%john%')
  .order('createdAt', { ascending: false })
  .limit(20)
  .execute();

// Complex queries with joins
const coursesWithDetails = await client
  .from('course')
  .transformColumns(true)
  .select('id', 'title', 'finalPrice')
  .leftJoin('course_category', { 
    select: ['categoryName', 'description'] 
  })
  .where('publishedAt', 'eq', true)
  .execute();

Mutation Methods

Transformation works for all mutation operations:

typescript
// INSERT with transformation
const newUser = await client
  .from('users')
  .transformColumns(true)
  .insert({
    firstName: 'Jane',        // → first_name
    lastName: 'Smith',        // → last_name
    emailAddress: 'jane@example.com', // → email_address
    isActive: true           // → is_active
  })
  .execute();

// UPDATE with transformation
const updatedUser = await client
  .from('users')
  .transformColumns(true)
  .update({
    firstName: 'Jane Updated',  // → first_name
    lastLoginAt: new Date()     // → last_login_at
  })
  .where('userId', 'eq', 1)      // → user_id
  .execute();

// UPSERT with transformation
await client
  .from('user_preferences')
  .transformColumns(true)
  .upsert({
    userId: 1,                // → user_id
    themePreference: 'dark',  // → theme_preference
    notificationsEnabled: true // → notifications_enabled
  })
  .execute();
typescript
// Repository mutations with transformation
const userRepo = client.getRepository<User>('users');
const preferencesRepo = client.getRepository<UserPreferences>('user_preferences');

// INSERT with repository save
const newUser = await userRepo.save({
  firstName: 'Jane',        // → first_name
  lastName: 'Smith',        // → last_name
  emailAddress: 'jane@example.com', // → email_address
  isActive: true           // → is_active
});

// Alternative: using insert method
const insertedUser = await userRepo.insert({
  firstName: 'Jane',
  lastName: 'Smith',
  emailAddress: 'jane@example.com',
  isActive: true
});

// UPDATE with repository
const updatedUser = await userRepo.update(
  { userId: 1 },              // → user_id
  {
    firstName: 'Jane Updated', // → first_name
    lastLoginAt: new Date()    // → last_login_at
  }
);

// Alternative: using save for updates
const existingUser = await userRepo.findOne({ userId: 1 });
if (existingUser) {
  existingUser.firstName = 'Jane Updated';
  existingUser.lastLoginAt = new Date();
  await userRepo.save(existingUser);
}

// UPSERT with repository save (automatic upsert behavior)
await preferencesRepo.save({
  userId: 1,                // → user_id
  themePreference: 'dark',  // → theme_preference
  notificationsEnabled: true // → notifications_enabled
});

// Custom repository methods with transformation
class UserRepository extends CustomRepositoryBase<User> {
  async updateProfile(userId: number, profileData: Partial<User>) {
    return this.update(
      { userId },             // → user_id
      {
        ...profileData,
        updatedAt: new Date() // → updated_at
      }
    );
  }

  async createUserWithPreferences(userData: Partial<User>, preferences: any) {
    const user = await this.save(userData);
    
    if (user) {
      await this.manager.getRepository('user_preferences').save({
        userId: user.userId,  // → user_id
        ...preferences
      });
    }
    
    return user;
  }
}

Bulk Operations

typescript
// Bulk insert with transformation
const results = await client
  .from('users')
  .transformColumns(true)
  .bulkInsert([
    { firstName: 'Alice', lastName: 'Johnson', isActive: true },
    { firstName: 'Bob', lastName: 'Wilson', isActive: false }
  ]);

// Each item in results.data will have camelCase properties
results.data.forEach(user => {
  console.log(user.firstName); // ✅ Transformed
});

Advanced Features

Nested Object Transformation

Transformation works recursively with nested objects and arrays:

typescript
interface Course {
  id: string;
  title: string;
  finalPrice: number;
  courseCategory: {
    categoryId: number;
    categoryName: string;
    parentCategory: {
      categoryId: number;
      categoryName: string;
    };
  };
  courseTags: Array<{
    tagId: number;
    tagName: string;
    tagColor: string;
  }>;
}

const course = await client
  .from<Course>('course')
  .transformColumns(true)
  .findOne({
    where: { id: 'course-uuid' },
    relations: ['courseCategory', 'courseTags'],
    select: [
      'id', 'title', 'finalPrice',
      'courseCategory.categoryName',
      'courseCategory.parentCategory.categoryName',
      'courseTags.tagName'
    ]
  });

// Response has fully transformed nested structure:
// {
//   id: 'course-uuid',
//   title: 'Course Title',
//   finalPrice: 99.99,
//   courseCategory: {
//     categoryName: 'Programming',
//     parentCategory: {
//       categoryName: 'Technology'
//     }
//   },
//   courseTags: [
//     { tagName: 'JavaScript' },
//     { tagName: 'TypeScript' }
//   ]
// }

Aliasing with Transformation

Combine aliases with transformation for custom response shapes:

typescript
const courses = await client
  .from('course')
  .transformColumns(true)
  .select([
    'id',
    'finalPrice AS price',      // Alias preserved
    'createdAt AS publishDate', // → created_at AS publish_date
    'courseCategory.categoryName AS category'
  ])
  .execute();

// Response structure:
// [{ 
//   id: '...', 
//   price: 99.99,           // Alias used as-is
//   publishDate: '...',     // Alias used as-is
//   category: 'Programming' // Nested field aliased
// }]

Raw Queries with Transformation

Even raw SQL-like queries benefit from transformation:

typescript
const results = await client
  .from('users')
  .transformColumns(true)
  .rawSelect('userId,firstName,lastName')
  .rawFilter({ 'createdAt': 'gte.2023-01-01' })
  .execute();
// Raw filters use snake_case, response gets camelCase

React Integration

Hook-based Queries

typescript
import { 
  useQuery, 
  useInfiniteQuery, 
  useQueryBuilder,
  useInsert, 
  useUpdate 
} from '@webcoded/pgrestify/react';

function UserList() {
  // Modern useQuery with transformation
  const { data: users, isLoading } = useQuery<User>({
    from: 'users',
    select: ['userId', 'firstName', 'lastName', 'isActive'],
    filter: { isActive: true },
    order: { column: 'createdAt', ascending: false },
    transformColumns: true,
    limit: 10
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {users?.map(user => (
        <div key={user.userId}>
          {user.firstName} {user.lastName}
          {user.isActive ? ' (Active)' : ' (Inactive)'}
        </div>
      ))}
    </div>
  );
}

Infinite Queries with Transformation

typescript
function InfiniteUserList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfiniteQuery<User>('users', {
    select: ['userId', 'firstName', 'lastName'],
    filter: { isActive: true },
    transformColumns: true,
    pageSize: 20,
    paginationType: 'offset'
  });

  return (
    <div>
      {data?.pages.map((page, pageIndex) => (
        <div key={pageIndex}>
          {page.map(user => (
            <div key={user.userId}>
              {user.firstName} {user.lastName}
            </div>
          ))}
        </div>
      ))}
      
      {hasNextPage && (
        <button onClick={() => fetchNextPage()}>
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

Pre-built Query with Hook

typescript
function UserDetail({ userId }: { userId: number }) {
  // Build query with transformation
  const query = client
    .from<User>('users')
    .transformColumns(true)
    .select('userId', 'firstName', 'lastName', 'emailAddress')
    .where('userId', 'eq', userId);

  // Use with hook
  const { data: user, error, isLoading } = useQueryBuilder(query);

  if (isLoading) return <div>Loading user...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.firstName} {user.lastName}</h1>
      <p>Email: {user.emailAddress}</p>
      <p>ID: {user.userId}</p>
    </div>
  );
}

Mutations with Transformation

typescript
function CreateUserForm() {
  const { mutate: createUser, isLoading } = useInsert<User>('users');
  const { mutate: updateUser } = useUpdate<User>('users');

  const handleCreate = () => {
    createUser({
      firstName: 'New',           // → first_name
      lastName: 'User',           // → last_name
      emailAddress: 'new@user.com', // → email_address
      isActive: true              // → is_active
    });
  };

  const handleUpdate = () => {
    updateUser({
      values: {
        firstName: 'Updated',     // → first_name
        isActive: false           // → is_active
      },
      where: { userId: 1 }        // → user_id
    });
  };

  return (
    <div>
      <button onClick={handleCreate} disabled={isLoading}>
        {isLoading ? 'Creating...' : 'Create User'}
      </button>
      <button onClick={handleUpdate}>
        Update User
      </button>
    </div>
  );
}

TypeScript Integration

Interface Definition

Define your interfaces using camelCase conventions:

typescript
interface User {
  userId: number;
  firstName: string;
  lastName: string;
  emailAddress: string;
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
  lastLoginAt?: string;
  
  // Nested objects
  userProfile?: {
    profileId: number;
    bioText: string;
    avatarUrl: string;
  };
  
  // Arrays of objects
  userPreferences?: Array<{
    preferenceId: number;
    preferenceName: string;
    preferenceValue: string;
  }>;
}

Type-Safe Queries

typescript
// TypeScript validates column names and types
const users = await client
  .from<User>('users')
  .transformColumns(true)
  .select('userId', 'firstName', 'emailAddress') // ✅ Type-checked
  .where('isActive', 'eq', true)                 // ✅ Type-checked
  .order('createdAt', { ascending: false })      // ✅ Type-checked
  .execute();

// users.data has correct TypeScript types
users.data.forEach(user => {
  console.log(user.firstName); // ✅ TypeScript knows this exists
  console.log(user.userId);    // ✅ TypeScript knows this is a number
});

Generic Repository Pattern

typescript
class UserService {
  constructor(private client: PostgRESTClient) {}

  async findByEmail(email: string): Promise<User | null> {
    return this.client
      .from<User>('users')
      .transformColumns(true)
      .findOne({
        where: { emailAddress: email },
        select: ['userId', 'firstName', 'lastName', 'isActive']
      });
  }

  async create(userData: Omit<User, 'userId' | 'createdAt'>): Promise<User> {
    const result = await this.client
      .from<User>('users')
      .transformColumns(true)
      .insert(userData)
      .single()
      .execute();

    if (result.error) throw new Error(result.error.message);
    return result.data!;
  }

  async updateProfile(userId: number, updates: Partial<User>): Promise<User> {
    const result = await this.client
      .from<User>('users')
      .transformColumns(true)
      .update(updates)
      .where('userId', 'eq', userId)
      .single()
      .execute();

    if (result.error) throw new Error(result.error.message);
    return result.data!;
  }
}
typescript
class UserRepository extends CustomRepositoryBase<User> {
  async findByEmail(email: string): Promise<User | null> {
    return this.findOne({
      where: { emailAddress: email },
      select: ['userId', 'firstName', 'lastName', 'isActive']
      // transformColumns inherited from client configuration
    });
  }

  async create(userData: Omit<User, 'userId' | 'createdAt'>): Promise<User> {
    return this.save({
      ...userData,
      createdAt: new Date().toISOString()
    });
  }

  async updateProfile(userId: number, updates: Partial<User>): Promise<User> {
    const existingUser = await this.findOne({ userId });
    if (!existingUser) {
      throw new Error('User not found');
    }

    return this.save({
      ...existingUser,
      ...updates,
      updatedAt: new Date().toISOString()
    });
  }

  async findActiveUsers(limit: number = 10): Promise<User[]> {
    return this.find({
      where: { isActive: true },
      select: ['userId', 'firstName', 'lastName', 'emailAddress'],
      order: { createdAt: 'DESC' },
      take: limit
    });
  }

  async createWithPreferences(
    userData: Omit<User, 'userId' | 'createdAt'>,
    preferences: any
  ): Promise<User> {
    // Use transaction for atomicity
    return this.manager.transaction(async (transactionalEntityManager) => {
      const userRepo = transactionalEntityManager.getRepository<User>('users');
      const prefRepo = transactionalEntityManager.getRepository('user_preferences');

      const user = await userRepo.save({
        ...userData,
        createdAt: new Date().toISOString()
      });

      await prefRepo.save({
        userId: user.userId,  // → user_id
        ...preferences
      });

      return user;
    });
  }
}

// Usage
const userRepo = client.getCustomRepository(UserRepository);

const user = await userRepo.findByEmail('john@example.com');
const newUser = await userRepo.create({
  firstName: 'John',
  lastName: 'Doe',
  emailAddress: 'john@example.com',
  isActive: true
});

Performance Considerations

Transformation Overhead

  • Minimal impact - Simple regex-based transformations
  • Efficient algorithms - O(n) complexity where n = number of keys
  • Memory usage - Creates new objects (immutable pattern)
  • Caching potential - Column name mappings could be cached

Best Practices

typescript
// ✅ Good: Enable globally if most queries need it
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: true
});

// ✅ Good: Disable for specific performance-critical queries
const largeBatchQuery = client
  .from('analytics_data')
  .transformColumns(false) // Skip transformation for performance
  .select('*')
  .limit(10000);

// ✅ Good: Use selective columns to reduce transformation overhead
const optimizedQuery = client
  .from('users')
  .transformColumns(true)
  .select('userId', 'firstName') // Only transform needed columns
  .limit(100);

// ❌ Avoid: Unnecessary transformation for simple queries
const simpleCount = client
  .from('users')
  .transformColumns(true) // Unnecessary for count queries
  .count();

Migration Strategies

Gradual Migration

If you have an existing codebase, migrate gradually:

typescript
// Phase 1: Keep existing code working
const client = createClient({
  url: 'http://localhost:3000',
  transformColumns: false // Keep disabled globally
});

// Phase 2: Enable for new features
const newFeatureQuery = client
  .from('new_table')
  .transformColumns(true) // Enable for new code
  .select('camelCaseField');

// Phase 3: Migrate existing queries one by one
const migratedQuery = client
  .from('existing_table')
  .transformColumns(true)
  .select('existingField'); // Update interfaces gradually

Interface Updates

Update your TypeScript interfaces gradually:

typescript
// Before (snake_case)
interface User {
  user_id: number;
  first_name: string;
  last_name: string;
  email_address: string;
}

// After (camelCase with transformation)
interface User {
  userId: number;
  firstName: string;
  lastName: string;
  emailAddress: string;
}

Troubleshooting

Common Issues

Issue: Data not being transformed

typescript
// ❌ Transformation not enabled
const client = createClient({ url: 'http://localhost:3000' });
const data = await client.from('users').execute();
// Solution: Enable transformation
const client = createClient({ url: 'http://localhost:3000', transformColumns: true });

Issue: Mixed naming conventions

typescript
// ❌ Mixing camelCase and snake_case
.select('firstName', 'last_name') // Inconsistent

// ✅ Use consistent naming
.select('firstName', 'lastName')   // All camelCase

Issue: Nested objects not transforming

typescript
// This is expected behavior - transformation is recursive
const data = await client
  .from('users')
  .transformColumns(true)
  .leftJoin('user_profile', { select: ['bio_text'] })
  .execute();

// Result will be:
// { userId: 1, userProfile: { bioText: '...' } }

Debug Tips

typescript
// 1. Check if transformation is enabled
const query = client.from('users').transformColumns(true);
// @ts-ignore - For debugging only
console.log('Transform enabled:', query.isColumnTransformEnabled?.());

// 2. Compare raw vs transformed queries
const rawQuery = client.from('users').transformColumns(false);
const transformedQuery = client.from('users').transformColumns(true);

// 3. Test with simple queries first
const simpleTest = await client
  .from('users')
  .transformColumns(true)
  .select('userId')  // Single field test
  .limit(1)
  .execute();

Examples

E-commerce Application

typescript
interface Product {
  productId: string;
  productName: string;
  unitPrice: number;
  isActive: boolean;
  createdAt: string;
  categoryId: number;
  
  // Relations
  productCategory?: {
    categoryId: number;
    categoryName: string;
    categorySlug: string;
  };
  
  productImages?: Array<{
    imageId: number;
    imageUrl: string;
    altText: string;
    sortOrder: number;
  }>;
}

// Product listing with category and images
const products = await client
  .from<Product>('products')
  .transformColumns(true)
  .find({
    where: { isActive: true },
    relations: ['productCategory', 'productImages'],
    select: [
      'productId', 'productName', 'unitPrice',
      'productCategory.categoryName',
      'productImages.imageUrl'
    ],
    order: { createdAt: 'DESC' },
    take: 20
  });

User Management System

typescript
interface User {
  userId: number;
  firstName: string;
  lastName: string;
  emailAddress: string;
  isActive: boolean;
  lastLoginAt?: string;
  
  userRoles?: Array<{
    roleId: number;
    roleName: string;
    permissions: string[];
  }>;
}

class UserService {
  constructor(private client: PostgRESTClient) {}

  async getUserWithRoles(userId: number): Promise<User | null> {
    return this.client
      .from<User>('users')
      .transformColumns(true)
      .findOne({
        where: { userId },
        relations: ['userRoles'],
        select: [
          'userId', 'firstName', 'lastName', 'emailAddress',
          'isActive', 'lastLoginAt',
          'userRoles.roleName', 'userRoles.permissions'
        ]
      });
  }

  async updateLastLogin(userId: number): Promise<void> {
    await this.client
      .from('users')
      .transformColumns(true)
      .update({ lastLoginAt: new Date().toISOString() })
      .where('userId', 'eq', userId)
      .execute();
  }
}

Summary

Column name transformation in PGRestify provides:

  • Seamless JavaScript integration with camelCase conventions
  • Database compatibility with PostgreSQL snake_case standards
  • Full feature support across all query and mutation methods
  • Deep nesting transformation for complex data structures
  • Type safety with complete TypeScript integration
  • Performance optimization with selective transformation
  • Migration flexibility for existing codebases

This feature bridges the gap between JavaScript naming conventions and PostgreSQL database schemas, providing a seamless development experience while maintaining database best practices.

Released under the MIT License.