Updating Records
Master data updates in PGRestify with both PostgREST syntax and ORM-style repositories for partial updates, conditional updates, bulk updates, optimistic locking, and return specifications.
Overview
Updating records in PGRestify provides flexible and safe data modification capabilities with two powerful approaches:
- 🎯 PostgREST Native Syntax: Direct
.update()
methods with condition chaining - 🏗️ ORM-Style Repository Pattern: Repository methods like
.save()
,.update()
, and query builder updates
Both approaches offer full type safety, conditional updates, and performance optimization.
Basic Record Updates
Single Record Update
typescript
import { createClient } from '@webcoded/pgrestify';
const client = createClient({
url: 'http://localhost:3000'
});
// Update a user by ID
const updatedUser = await client
.from('users')
.update({
name: 'John Updated',
age: 31,
updated_at: new Date().toISOString()
})
.eq('id', 123)
.execute();
console.log('Updated user:', updatedUser.data);
typescript
import { createClient } from '@webcoded/pgrestify';
const client = createClient({
url: 'http://localhost:3000'
});
// Get repository for users table
const userRepo = client.getRepository<User>('users');
// Method 1: Update using save (if you have the full entity)
const user = await userRepo.findOne({ id: 123 });
if (user) {
user.name = 'John Updated';
user.age = 31;
user.updated_at = new Date().toISOString();
const updatedUser = await userRepo.save(user);
console.log('Updated user:', updatedUser);
}
// Method 2: Direct update with conditions
const updatedUsers = await userRepo.update(
{ id: 123 }, // where condition
{
name: 'John Updated',
age: 31,
updated_at: new Date().toISOString()
}
);
console.log('Updated users:', updatedUsers);
Partial Updates
typescript
// Update only specific fields
const partialUpdate = await client
.from('users')
.update({
last_login: new Date().toISOString()
// Only updates last_login, other fields remain unchanged
})
.eq('id', 123)
.execute();
// Update nested JSON fields
const jsonUpdate = await client
.from('users')
.update({
'preferences->theme': 'dark',
'preferences->language': 'en'
})
.eq('id', 123)
.execute();
typescript
// Repository partial updates
const userRepo = client.getRepository<User>('users');
// Update only specific fields
const partialUpdate = await userRepo.update(
{ id: 123 },
{
last_login: new Date().toISOString()
// Only updates last_login, other fields remain unchanged
}
);
// Update using query builder for complex conditions
const queryBuilderUpdate = await userRepo
.createQueryBuilder()
.update({
last_login: new Date().toISOString(),
status: 'active'
})
.where('id = :id', { id: 123 })
.execute();
// Note: JSON field updates with repository pattern
// would typically involve reading, modifying, and saving
const user = await userRepo.findOne({ id: 123 });
if (user && user.preferences) {
user.preferences.theme = 'dark';
user.preferences.language = 'en';
await userRepo.save(user);
}
Type-Safe Updates
typescript
interface User {
id: number;
name: string;
email: string;
age?: number;
is_active: boolean;
updated_at: string;
}
// Type-safe update
const typedUpdate = await client
.from<User>('users')
.update({
name: 'Jane Updated',
age: 28
// TypeScript ensures only valid User fields can be updated
})
.eq('id', 456)
.execute();
// TypeScript prevents invalid updates
// .update({ invalid_field: 'value' }) // ❌ TypeScript error
typescript
interface User {
id: number;
name: string;
email: string;
age?: number;
is_active: boolean;
updated_at: string;
}
// Get typed repository
const userRepo = client.getRepository<User>('users');
// Type-safe repository update
const typedUpdate = await userRepo.update(
{ id: 456 },
{
name: 'Jane Updated',
age: 28
// TypeScript ensures only valid User fields can be updated
}
);
// TypeScript prevents invalid updates
// userRepo.update({ id: 456 }, { invalid_field: 'value' }); // ❌ TypeScript error
// Type-safe query builder update
const qbUpdate = await userRepo
.createQueryBuilder()
.update({
name: 'Jane Updated',
age: 28
})
.where('id = :id', { id: 456 })
.execute();
Conditional Updates
Single Condition Updates
typescript
// Update users with specific condition
const conditionalUpdate = await client
.from('users')
.update({
status: 'verified',
verified_at: new Date().toISOString()
})
.eq('email_verified', true)
.is('verified_at', null) // Only update unverified users
.execute();
typescript
// Repository conditional updates
const userRepo = client.getRepository<User>('users');
// Update with multiple conditions using query builder
const conditionalUpdate = await userRepo
.createQueryBuilder()
.update({
status: 'verified',
verified_at: new Date().toISOString()
})
.where('email_verified = :verified', { verified: true })
.andWhere('verified_at IS NULL')
.execute();
// Alternative: Find and update pattern
const usersToUpdate = await userRepo
.createQueryBuilder()
.where('email_verified = :verified', { verified: true })
.andWhere('verified_at IS NULL')
.getMany();
for (const user of usersToUpdate) {
user.status = 'verified';
user.verified_at = new Date().toISOString();
await userRepo.save(user);
}
Multiple Condition Updates
typescript
// Update with complex conditions
const complexUpdate = await client
.from('products')
.update({
status: 'discontinued',
updated_at: new Date().toISOString()
})
.lt('stock_quantity', 5)
.eq('is_active', true)
.lt('last_sold', '2023-01-01')
.execute();
// Update with OR conditions
const orConditionUpdate = await client
.from('orders')
.update({
priority: 'high',
updated_at: new Date().toISOString()
})
.or('total_amount.gte.1000,customer_tier.eq.premium')
.execute();
typescript
// Repository complex condition updates
const productRepo = client.getRepository<Product>('products');
const orderRepo = client.getRepository<Order>('orders');
// Update with complex conditions using query builder
const complexUpdate = await productRepo
.createQueryBuilder()
.update({
status: 'discontinued',
updated_at: new Date().toISOString()
})
.where('stock_quantity < :stock', { stock: 5 })
.andWhere('is_active = :active', { active: true })
.andWhere('last_sold < :date', { date: '2023-01-01' })
.execute();
// Update with OR conditions using query builder
const orConditionUpdate = await orderRepo
.createQueryBuilder()
.update({
priority: 'high',
updated_at: new Date().toISOString()
})
.where('total_amount >= :amount', { amount: 1000 })
.orWhere('customer_tier = :tier', { tier: 'premium' })
.execute();
// Alternative: Complex conditions with custom repository
class ProductRepository extends CustomRepositoryBase<Product> {
async discontinueOldLowStockProducts() {
return this.createQueryBuilder()
.update({
status: 'discontinued',
updated_at: new Date().toISOString()
})
.where('stock_quantity < :stock', { stock: 5 })
.andWhere('is_active = :active', { active: true })
.andWhere('last_sold < :date', { date: '2023-01-01' })
.execute();
}
}
const customProductRepo = client.getCustomRepository(ProductRepository);
const result = await customProductRepo.discontinueOldLowStockProducts();
Existence-Based Updates
typescript
// Update only if record exists
const safeUpdate = async (userId: number, updates: Partial<User>) => {
// First check if user exists
const user = await client
.from('users')
.select('id')
.eq('id', userId)
.single()
.execute();
if (user.error || !user.data) {
throw new Error(`User with ID ${userId} not found`);
}
// Proceed with update
const result = await client
.from('users')
.update(updates)
.eq('id', userId)
.execute();
if (result.error) {
throw new Error(`Update failed: ${result.error.message}`);
}
return result.data[0];
};
// Usage
try {
const updatedUser = await safeUpdate(123, {
name: 'Safe Updated Name',
age: 35
});
console.log('User updated safely:', updatedUser);
} catch (error) {
console.error('Update failed:', error.message);
}
typescript
// Repository-based existence updates
const userRepo = client.getRepository<User>('users');
// Method 1: Repository built-in existence checking
const safeUpdateRepo = async (userId: number, updates: Partial<User>) => {
// findOne returns null if not found
const existingUser = await userRepo.findOne({ id: userId });
if (!existingUser) {
throw new Error(`User with ID ${userId} not found`);
}
// Update existing user
const updatedUser = { ...existingUser, ...updates };
return await userRepo.save(updatedUser);
};
// Method 2: Direct update with existence validation
const safeDirectUpdate = async (userId: number, updates: Partial<User>) => {
const result = await userRepo.update({ id: userId }, updates);
if (result.length === 0) {
throw new Error(`User with ID ${userId} not found`);
}
return result[0];
};
// Method 3: Custom repository with built-in safety
class UserRepository extends CustomRepositoryBase<User> {
async safeUpdate(userId: number, updates: Partial<User>) {
const user = await this.findOne({ id: userId });
if (!user) {
throw new Error(`User with ID ${userId} not found`);
}
return this.save({ ...user, ...updates });
}
async updateIfExists(userId: number, updates: Partial<User>) {
const result = await this.update({ id: userId }, updates);
return result.length > 0 ? result[0] : null;
}
}
const customUserRepo = client.getCustomRepository(UserRepository);
// Usage
try {
const updatedUser = await customUserRepo.safeUpdate(123, {
name: 'Safe Updated Name',
age: 35
});
console.log('User updated safely:', updatedUser);
} catch (error) {
console.error('Update failed:', error.message);
}
// Alternative: Silent update (returns null if not found)
const result = await customUserRepo.updateIfExists(123, {
name: 'Safe Updated Name',
age: 35
});
if (result) {
console.log('User updated:', result);
} else {
console.log('User not found, no update performed');
}
Bulk Updates
Multiple Records Update
typescript
// Update multiple records with same values
const bulkStatusUpdate = await client
.from('orders')
.update({
status: 'processing',
processing_started_at: new Date().toISOString()
})
.eq('status', 'pending')
.gte('created_at', '2024-01-01')
.execute();
console.log(`Updated ${bulkStatusUpdate.data.length} orders to processing`);
typescript
// Repository bulk updates
const orderRepo = client.getRepository<Order>('orders');
// Method 1: Repository update with conditions
const bulkStatusUpdate = await orderRepo.update(
{
status: 'pending',
created_at: { gte: '2024-01-01' }
},
{
status: 'processing',
processing_started_at: new Date().toISOString()
}
);
console.log(`Updated ${bulkStatusUpdate.length} orders to processing`);
// Method 2: Query builder bulk update
const qbBulkUpdate = await orderRepo
.createQueryBuilder()
.update({
status: 'processing',
processing_started_at: new Date().toISOString()
})
.where('status = :status', { status: 'pending' })
.andWhere('created_at >= :date', { date: '2024-01-01' })
.execute();
// Method 3: Custom repository with business logic
class OrderRepository extends CustomRepositoryBase<Order> {
async startProcessingPendingOrders(sinceDate: string) {
return this.createQueryBuilder()
.update({
status: 'processing',
processing_started_at: new Date().toISOString()
})
.where('status = :status', { status: 'pending' })
.andWhere('created_at >= :date', { date: sinceDate })
.execute();
}
async bulkUpdateStatus(fromStatus: string, toStatus: string, additionalUpdates: Partial<Order> = {}) {
return this.update(
{ status: fromStatus },
{
...additionalUpdates,
status: toStatus,
updated_at: new Date().toISOString()
}
);
}
}
const customOrderRepo = client.getCustomRepository(OrderRepository);
const result = await customOrderRepo.startProcessingPendingOrders('2024-01-01');
console.log(`Updated ${result.length} orders to processing`);
Conditional Bulk Updates
typescript
// Bulk update with complex conditions
const bulkPriceUpdate = await client
.from('products')
.update({
price: client.raw('price * 1.1'), // Increase price by 10%
updated_at: new Date().toISOString()
})
.eq('category', 'electronics')
.gte('created_at', '2023-01-01')
.execute();
// Update with different values based on conditions
const tieredUpdate = async () => {
// Update premium customers
const premiumUpdate = await client
.from('customers')
.update({
discount_rate: 0.15,
tier: 'premium_plus'
})
.eq('tier', 'premium')
.gte('total_spent', 10000)
.execute();
// Update regular customers
const regularUpdate = await client
.from('customers')
.update({
discount_rate: 0.05,
tier: 'regular_plus'
})
.eq('tier', 'regular')
.gte('total_spent', 5000)
.execute();
return {
premiumUpdated: premiumUpdate.data.length,
regularUpdated: regularUpdate.data.length
};
};
typescript
// Repository conditional bulk updates
const productRepo = client.getRepository<Product>('products');
const customerRepo = client.getRepository<Customer>('customers');
// Bulk price update with raw SQL expressions
const bulkPriceUpdate = await productRepo
.createQueryBuilder()
.update({
price: client.raw('price * 1.1'), // Increase price by 10%
updated_at: new Date().toISOString()
})
.where('category = :category', { category: 'electronics' })
.andWhere('created_at >= :date', { date: '2023-01-01' })
.execute();
// Tiered update with custom repository methods
class CustomerRepository extends CustomRepositoryBase<Customer> {
async upgradePremiumCustomers(spentThreshold: number = 10000) {
return this.update(
{
tier: 'premium',
total_spent: { gte: spentThreshold }
},
{
discount_rate: 0.15,
tier: 'premium_plus',
updated_at: new Date().toISOString()
}
);
}
async upgradeRegularCustomers(spentThreshold: number = 5000) {
return this.update(
{
tier: 'regular',
total_spent: { gte: spentThreshold }
},
{
discount_rate: 0.05,
tier: 'regular_plus',
updated_at: new Date().toISOString()
}
);
}
async performTieredUpdate() {
const [premiumUpdate, regularUpdate] = await Promise.all([
this.upgradePremiumCustomers(10000),
this.upgradeRegularCustomers(5000)
]);
return {
premiumUpdated: premiumUpdate.length,
regularUpdated: regularUpdate.length
};
}
}
// Usage with custom repository
const customCustomerRepo = client.getCustomRepository(CustomerRepository);
const tieredUpdateResult = await customCustomerRepo.performTieredUpdate();
// Alternative: Using standard repository with parallel updates
const tieredUpdate = async () => {
const [premiumUpdate, regularUpdate] = await Promise.all([
customerRepo.update(
{ tier: 'premium', total_spent: { gte: 10000 } },
{ discount_rate: 0.15, tier: 'premium_plus' }
),
customerRepo.update(
{ tier: 'regular', total_spent: { gte: 5000 } },
{ discount_rate: 0.05, tier: 'regular_plus' }
)
]);
return {
premiumUpdated: premiumUpdate.length,
regularUpdated: regularUpdate.length
};
};
Batch Update with Different Values
typescript
// Update multiple records with different values
const batchUpdateWithDifferentValues = async (updates: Array<{ id: number; data: any }>) => {
const results = [];
const errors = [];
for (const { id, data } of updates) {
try {
const result = await client
.from('users')
.update(data)
.eq('id', id)
.single()
.execute();
if (result.error) {
errors.push({ id, error: result.error.message });
} else {
results.push(result.data);
}
} catch (error) {
errors.push({ id, error: error.message });
}
}
return { results, errors };
};
// Usage
const updateBatch = [
{ id: 1, data: { name: 'User One Updated', age: 25 } },
{ id: 2, data: { name: 'User Two Updated', age: 30 } },
{ id: 3, data: { status: 'inactive' } }
];
const batchResult = await batchUpdateWithDifferentValues(updateBatch);
console.log(`Success: ${batchResult.results.length}, Errors: ${batchResult.errors.length}`);
typescript
// Repository batch updates with different values
const userRepo = client.getRepository<User>('users');
// Method 1: Sequential updates with repository
const batchUpdateSequential = async (updates: Array<{ id: number; data: Partial<User> }>) => {
const results = [];
const errors = [];
for (const { id, data } of updates) {
try {
const result = await userRepo.update({ id }, data);
if (result.length > 0) {
results.push(result[0]);
} else {
errors.push({ id, error: 'User not found' });
}
} catch (error) {
errors.push({ id, error: error.message });
}
}
return { results, errors };
};
// Method 2: Parallel updates with Promise.allSettled
const batchUpdateParallel = async (updates: Array<{ id: number; data: Partial<User> }>) => {
const updatePromises = updates.map(async ({ id, data }) => {
try {
const result = await userRepo.update({ id }, data);
return { id, success: true, data: result[0] };
} catch (error) {
return { id, success: false, error: error.message };
}
});
const results = await Promise.allSettled(updatePromises);
const successful = results
.filter((result): result is PromiseFulfilledResult<any> => result.status === 'fulfilled')
.map(result => result.value)
.filter(value => value.success);
const failed = results
.filter((result): result is PromiseFulfilledResult<any> => result.status === 'fulfilled')
.map(result => result.value)
.filter(value => !value.success);
return {
results: successful.map(s => s.data),
errors: failed.map(f => ({ id: f.id, error: f.error }))
};
};
// Method 3: Custom repository with batch update logic
class UserRepository extends CustomRepositoryBase<User> {
async batchUpdate(updates: Array<{ id: number; data: Partial<User> }>) {
const results = [];
const errors = [];
// Group updates by similarity to optimize
const groupedUpdates = this.groupUpdatesByFields(updates);
for (const group of groupedUpdates) {
try {
if (group.length === 1) {
// Single update
const { id, data } = group[0];
const result = await this.update({ id }, data);
if (result.length > 0) {
results.push({ id, data: result[0] });
} else {
errors.push({ id, error: 'User not found' });
}
} else {
// Bulk update if all have same fields
const sampleData = group[0].data;
const allSameFields = group.every(item =>
Object.keys(item.data).sort().join(',') === Object.keys(sampleData).sort().join(',')
);
if (allSameFields && this.allHaveSameValues(group)) {
// True bulk update
const ids = group.map(item => item.id);
const bulkResult = await this.update(
{ id: { in: ids } },
sampleData
);
results.push(...bulkResult.map(data => ({ id: data.id, data })));
} else {
// Individual updates
for (const { id, data } of group) {
const result = await this.update({ id }, data);
if (result.length > 0) {
results.push({ id, data: result[0] });
} else {
errors.push({ id, error: 'User not found' });
}
}
}
}
} catch (error) {
for (const { id } of group) {
errors.push({ id, error: error.message });
}
}
}
return { results, errors };
}
private groupUpdatesByFields(updates: Array<{ id: number; data: any }>) {
const groups = new Map();
for (const update of updates) {
const fieldKey = Object.keys(update.data).sort().join(',');
if (!groups.has(fieldKey)) {
groups.set(fieldKey, []);
}
groups.get(fieldKey).push(update);
}
return Array.from(groups.values());
}
private allHaveSameValues(group: Array<{ id: number; data: any }>) {
if (group.length <= 1) return true;
const first = JSON.stringify(group[0].data);
return group.every(item => JSON.stringify(item.data) === first);
}
}
// Usage
const customUserRepo = client.getCustomRepository(UserRepository);
const updateBatch = [
{ id: 1, data: { name: 'User One Updated', age: 25 } },
{ id: 2, data: { name: 'User Two Updated', age: 30 } },
{ id: 3, data: { status: 'inactive' } }
];
const batchResult = await customUserRepo.batchUpdate(updateBatch);
console.log(`Success: ${batchResult.results.length}, Errors: ${batchResult.errors.length}`);
// Simple parallel update
const parallelResult = await batchUpdateParallel(updateBatch);
console.log(`Success: ${parallelResult.results.length}, Errors: ${parallelResult.errors.length}`);
Optimistic Locking
Version-Based Locking
typescript
// Optimistic locking with version field
const optimisticUpdate = async (id: number, updates: any, expectedVersion: number) => {
const result = await client
.from('documents')
.update({
...updates,
version: expectedVersion + 1,
updated_at: new Date().toISOString()
})
.eq('id', id)
.eq('version', expectedVersion) // Only update if version matches
.execute();
if (result.data.length === 0) {
throw new Error('Document was modified by another user. Please refresh and try again.');
}
return result.data[0];
};
// Usage with retry logic
const updateWithRetry = async (id: number, updates: any, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Get current version
const current = await client
.from('documents')
.select('version')
.eq('id', id)
.single()
.execute();
if (current.error) {
throw new Error('Document not found');
}
// Attempt update with current version
return await optimisticUpdate(id, updates, current.data.version);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.log(`Update attempt ${attempt} failed, retrying...`);
// Brief delay before retry
await new Promise(resolve => setTimeout(resolve, 100));
}
}
};
Timestamp-Based Locking
typescript
// Optimistic locking with timestamp
const timestampBasedUpdate = async (id: number, updates: any, expectedTimestamp: string) => {
const result = await client
.from('posts')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.eq('updated_at', expectedTimestamp)
.execute();
if (result.data.length === 0) {
// Check if record exists or was modified
const current = await client
.from('posts')
.select('id, updated_at')
.eq('id', id)
.single()
.execute();
if (current.error) {
throw new Error('Record not found');
}
throw new Error(`Record was modified at ${current.data.updated_at}, expected ${expectedTimestamp}`);
}
return result.data[0];
};
Return Specifications
Controlling Returned Data
typescript
// Return all updated fields (default)
const fullReturn = await client
.from('products')
.update({
name: 'Updated Product',
price: 199.99
})
.eq('id', 456)
.execute();
// Return only specific fields
const limitedReturn = await client
.from('products')
.update({
name: 'Updated Product',
price: 199.99,
updated_at: new Date().toISOString()
})
.eq('id', 456)
.select('id, name, price, updated_at')
.execute();
// Return nothing for performance
const noReturn = await client
.from('products')
.update({
view_count: client.raw('view_count + 1')
})
.eq('id', 456)
.select('') // Minimal return
.execute();
typescript
// Repository return control
const productRepo = client.getRepository<Product>('products');
// Return all updated fields (default behavior)
const fullReturn = await productRepo.update(
{ id: 456 },
{
name: 'Updated Product',
price: 199.99
}
);
// Repository updates always return the full entity by default
// For partial returns, use query builder with select
const limitedReturn = await productRepo
.createQueryBuilder()
.update({
name: 'Updated Product',
price: 199.99,
updated_at: new Date().toISOString()
})
.where('id = :id', { id: 456 })
.select(['id', 'name', 'price', 'updated_at'])
.execute();
// For performance updates with minimal return
const performanceUpdate = await productRepo
.createQueryBuilder()
.update({
view_count: client.raw('view_count + 1')
})
.where('id = :id', { id: 456 })
.select(['id']) // Minimal return
.execute();
// Custom repository for controlled returns
class ProductRepository extends CustomRepositoryBase<Product> {
async incrementViewCount(id: number): Promise<{ id: number; view_count: number }> {
const result = await this.createQueryBuilder()
.update({
view_count: client.raw('view_count + 1')
})
.where('id = :id', { id })
.select(['id', 'view_count'])
.execute();
return result[0];
}
async updateWithMinimalReturn(id: number, updates: Partial<Product>) {
return this.createQueryBuilder()
.update(updates)
.where('id = :id', { id })
.select(['id', 'updated_at'])
.execute();
}
}
Single Record Return
typescript
// Ensure single record update and return
const singleUpdate = await client
.from('users')
.update({
last_login: new Date().toISOString(),
login_count: client.raw('login_count + 1')
})
.eq('id', 123)
.single() // Ensures single object return
.execute();
console.log('Updated user:', singleUpdate.data);
typescript
// Repository single record returns
const userRepo = client.getRepository<User>('users');
// Method 1: Repository update (always returns array, get first item)
const updateResult = await userRepo.update(
{ id: 123 },
{
last_login: new Date().toISOString(),
login_count: client.raw('login_count + 1')
}
);
const singleUpdate = updateResult[0]; // Get first (and should be only) result
console.log('Updated user:', singleUpdate);
// Method 2: Custom repository method that ensures single return
class UserRepository extends CustomRepositoryBase<User> {
async updateSingleUser(id: number, updates: Partial<User>): Promise<User> {
const result = await this.update({ id }, updates);
if (result.length === 0) {
throw new Error(`User with ID ${id} not found`);
}
if (result.length > 1) {
throw new Error(`Multiple users found with ID ${id}`);
}
return result[0];
}
async updateLoginInfo(id: number): Promise<User> {
const result = await this.createQueryBuilder()
.update({
last_login: new Date().toISOString(),
login_count: client.raw('login_count + 1')
})
.where('id = :id', { id })
.execute();
if (result.length === 0) {
throw new Error('User not found');
}
return result[0];
}
}
const customUserRepo = client.getCustomRepository(UserRepository);
// Usage with guaranteed single return
try {
const updatedUser = await customUserRepo.updateLoginInfo(123);
console.log('Updated user:', updatedUser);
} catch (error) {
console.error('Update failed:', error.message);
}
// Method 3: Using save for single record updates
const user = await userRepo.findOne({ id: 123 });
if (user) {
user.last_login = new Date().toISOString();
user.login_count = (user.login_count || 0) + 1;
const updatedUser = await userRepo.save(user);
console.log('Updated user:', updatedUser);
}
Advanced Update Patterns
Increment/Decrement Operations
typescript
// Increment counters
const incrementCounters = await client
.from('posts')
.update({
view_count: client.raw('view_count + 1'),
like_count: client.raw('like_count + 1'),
updated_at: new Date().toISOString()
})
.eq('id', 789)
.execute();
// Decrement stock
const decrementStock = await client
.from('products')
.update({
stock_quantity: client.raw('stock_quantity - 1'),
updated_at: new Date().toISOString()
})
.eq('id', 456)
.gte('stock_quantity', 1) // Only if stock available
.execute();
if (decrementStock.data.length === 0) {
throw new Error('Insufficient stock');
}
Conditional Field Updates
typescript
// Update fields based on conditions
const conditionalFieldUpdate = await client
.from('orders')
.update({
status: 'shipped',
shipped_at: new Date().toISOString(),
// Use PostgreSQL CASE expression for conditional updates
tracking_number: client.raw(`
CASE
WHEN shipping_method = 'express' THEN 'EXP' || nextval('tracking_seq')
WHEN shipping_method = 'standard' THEN 'STD' || nextval('tracking_seq')
ELSE 'REG' || nextval('tracking_seq')
END
`)
})
.eq('status', 'processing')
.execute();
JSON/JSONB Updates
typescript
// Update JSON fields
const jsonFieldUpdate = await client
.from('users')
.update({
// Update specific JSON keys
'preferences->theme': 'dark',
'preferences->notifications->email': false,
// Update entire JSON object
metadata: {
last_updated: new Date().toISOString(),
update_source: 'user_preferences',
version: 2
}
})
.eq('id', 123)
.execute();
// Merge JSON objects
const mergeJsonUpdate = await client
.from('user_settings')
.update({
// Use PostgreSQL jsonb_set for complex updates
settings: client.raw(`
jsonb_set(
jsonb_set(settings, '{ui,theme}', '"dark"'),
'{notifications,email}', 'false'
)
`)
})
.eq('user_id', 123)
.execute();
Array Updates
typescript
// Update array fields
const arrayUpdate = await client
.from('posts')
.update({
tags: ['javascript', 'typescript', 'react', 'hooks'], // Replace entire array
updated_at: new Date().toISOString()
})
.eq('id', 789)
.execute();
// Append to array using PostgreSQL functions
const appendToArray = await client
.from('posts')
.update({
tags: client.raw("array_append(tags, 'new-tag')"),
categories: client.raw("array_cat(categories, ARRAY[4, 5])")
})
.eq('id', 789)
.execute();
// Remove from array
const removeFromArray = await client
.from('posts')
.update({
tags: client.raw("array_remove(tags, 'old-tag')"),
updated_at: new Date().toISOString()
})
.eq('id', 789)
.execute();
Error Handling and Validation
Comprehensive Error Handling
typescript
interface UpdateResult<T> {
success: boolean;
data?: T[];
error?: string;
updatedCount?: number;
}
const safeUpdate = async <T>(
table: string,
updates: Partial<T>,
conditions: Record<string, any>
): Promise<UpdateResult<T>> => {
try {
let query = client.from(table).update(updates);
// Apply conditions
Object.entries(conditions).forEach(([key, value]) => {
query = query.eq(key, value);
});
const result = await query.execute();
if (result.error) {
return {
success: false,
error: result.error.message,
updatedCount: 0
};
}
return {
success: true,
data: result.data,
updatedCount: result.data.length
};
} catch (error) {
return {
success: false,
error: error.message,
updatedCount: 0
};
}
};
// Usage
const updateResult = await safeUpdate('users',
{ name: 'Updated Name', age: 30 },
{ id: 123 }
);
if (updateResult.success) {
console.log(`Updated ${updateResult.updatedCount} records`);
} else {
console.error('Update failed:', updateResult.error);
}
Input Validation
typescript
// Validate update data
const validateUpdateData = (updates: any): string[] => {
const errors: string[] = [];
if (updates.email && !/\S+@\S+\.\S+/.test(updates.email)) {
errors.push('Invalid email format');
}
if (updates.age && (updates.age < 0 || updates.age > 150)) {
errors.push('Age must be between 0 and 150');
}
if (updates.price && updates.price < 0) {
errors.push('Price cannot be negative');
}
return errors;
};
// Validated update
const updateWithValidation = async (id: number, updates: any) => {
const validationErrors = validateUpdateData(updates);
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join(', ')}`);
}
return client
.from('users')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.execute();
};
Conflict Resolution
typescript
// Handle update conflicts gracefully
const updateWithConflictResolution = async (
id: number,
updates: any,
strategy: 'overwrite' | 'merge' | 'fail' = 'merge'
) => {
try {
// Get current data
const current = await client
.from('documents')
.select('*')
.eq('id', id)
.single()
.execute();
if (current.error) {
throw new Error('Document not found');
}
let finalUpdates = updates;
switch (strategy) {
case 'overwrite':
// Use updates as-is
break;
case 'merge':
// Merge with current data
finalUpdates = { ...current.data, ...updates };
break;
case 'fail':
// Check if any fields have changed
const hasConflicts = Object.keys(updates).some(
key => current.data[key] !== updates[key] &&
current.data.updated_at > new Date(Date.now() - 60000) // Modified in last minute
);
if (hasConflicts) {
throw new Error('Concurrent modification detected');
}
break;
}
const result = await client
.from('documents')
.update({
...finalUpdates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.execute();
return result.data[0];
} catch (error) {
console.error('Update conflict resolution failed:', error);
throw error;
}
};
Performance Optimization
Efficient Update Patterns
typescript
// Batch updates for better performance
const efficientBulkUpdate = async (updates: Array<{id: number, data: any}>) => {
// Group updates by similar patterns
const grouped = updates.reduce((acc, update) => {
const keys = Object.keys(update.data).sort().join(',');
if (!acc[keys]) acc[keys] = [];
acc[keys].push(update);
return acc;
}, {} as Record<string, typeof updates>);
const results = [];
// Process each group
for (const [keys, group] of Object.entries(grouped)) {
if (group.length === 1) {
// Single update
const { id, data } = group[0];
const result = await client
.from('users')
.update(data)
.eq('id', id)
.execute();
results.push(...result.data);
} else {
// If all updates have same data, do bulk update
const sampleData = group[0].data;
const allSame = group.every(item =>
JSON.stringify(item.data) === JSON.stringify(sampleData)
);
if (allSame) {
const ids = group.map(item => item.id);
const result = await client
.from('users')
.update(sampleData)
.in('id', ids)
.execute();
results.push(...result.data);
} else {
// Individual updates for different data
for (const { id, data } of group) {
const result = await client
.from('users')
.update(data)
.eq('id', id)
.execute();
results.push(...result.data);
}
}
}
}
return results;
};
Minimal Data Updates
typescript
// Update only changed fields
const updateOnlyChanged = async (id: number, newData: any) => {
// Get current data
const current = await client
.from('users')
.select('*')
.eq('id', id)
.single()
.execute();
if (current.error) {
throw new Error('Record not found');
}
// Find only changed fields
const changes: any = {};
Object.keys(newData).forEach(key => {
if (current.data[key] !== newData[key]) {
changes[key] = newData[key];
}
});
// Only update if there are changes
if (Object.keys(changes).length === 0) {
console.log('No changes detected, skipping update');
return current.data;
}
changes.updated_at = new Date().toISOString();
const result = await client
.from('users')
.update(changes)
.eq('id', id)
.single()
.execute();
return result.data;
};
Summary
PGRestify's record update capabilities provide:
- Flexible Updates: Partial, conditional, and bulk update operations
- Type Safety: Full TypeScript support for update operations
- Optimistic Locking: Version and timestamp-based concurrency control
- Advanced Patterns: JSON/JSONB updates, array operations, and SQL expressions
- Error Handling: Comprehensive error management and validation
- Performance: Efficient bulk operations and minimal data transfer
- Conflict Resolution: Strategies for handling concurrent modifications
- Safe Operations: Existence checks and constraint handling
Master these update patterns to build robust data modification workflows that handle concurrency, maintain data integrity, and perform efficiently at scale.