/** * PostgreSQL AddressRepository. All operations are scoped by user_id, so a * caller can never read or mutate another user's address even with a valid id. */ import type pg from 'pg'; import type { AddressRepository } from '../domain/ports.js'; import type { Address, AddressPatch, NewAddress } from '../domain/address.js'; interface AddressRow { id: string; user_id: string; label: string | null; recipient_name: string; street: string; city: string; postal_code: string; country: string; is_default: boolean; created_at: Date; updated_at: Date; } /** Whitelist of updatable columns -> input key. Prevents SQL building from input. */ const UPDATABLE: ReadonlyArray<[keyof AddressPatch, string]> = [ ['label', 'label'], ['recipientName', 'recipient_name'], ['street', 'street'], ['city', 'city'], ['postalCode', 'postal_code'], ['country', 'country'], ['isDefault', 'is_default'], ]; export class PgAddressRepository implements AddressRepository { constructor(private readonly pool: pg.Pool) {} async listByUserId(userId: string): Promise { const result = await this.pool.query( `SELECT * FROM users_addresses WHERE user_id = $1 ORDER BY is_default DESC, created_at`, [userId], ); return result.rows.map(toAddress); } async create(userId: string, input: NewAddress): Promise
{ const result = await this.pool.query( `INSERT INTO users_addresses (user_id, label, recipient_name, street, city, postal_code, country, is_default) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, [ userId, input.label ?? null, input.recipientName, input.street, input.city, input.postalCode, input.country, input.isDefault ?? false, ], ); const row = result.rows[0]; if (!row) { throw new Error('users_addresses INSERT returned no row'); } return toAddress(row); } async update( userId: string, addressId: string, patch: AddressPatch, ): Promise
{ const setClauses: string[] = []; const values: unknown[] = []; for (const [key, column] of UPDATABLE) { const value = patch[key]; if (value !== undefined) { values.push(value); setClauses.push(`${column} = $${values.length}`); } } if (setClauses.length === 0) { return this.findByIdScoped(userId, addressId); } values.push(userId, addressId); const result = await this.pool.query( `UPDATE users_addresses SET ${setClauses.join(', ')}, updated_at = now() WHERE user_id = $${values.length - 1} AND id = $${values.length} RETURNING *`, values, ); const row = result.rows[0]; return row ? toAddress(row) : undefined; } async delete(userId: string, addressId: string): Promise { const result = await this.pool.query( `DELETE FROM users_addresses WHERE user_id = $1 AND id = $2`, [userId, addressId], ); return (result.rowCount ?? 0) > 0; } private async findByIdScoped(userId: string, addressId: string): Promise
{ const result = await this.pool.query( `SELECT * FROM users_addresses WHERE user_id = $1 AND id = $2`, [userId, addressId], ); const row = result.rows[0]; return row ? toAddress(row) : undefined; } } function toAddress(row: AddressRow): Address { return { id: row.id, userId: row.user_id, label: row.label, recipientName: row.recipient_name, street: row.street, city: row.city, postalCode: row.postal_code, country: row.country, isDefault: row.is_default, createdAt: row.created_at, updatedAt: row.updated_at, }; }