123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
import type pg from 'pg';
|
|
import { BrandSlugAlreadyExistsError } from '../domain/errors.js';
|
|
import type { Brand, BrandPatch, NewBrand } from '../domain/brand.js';
|
|
import type { BrandRepository } from '../domain/ports.js';
|
|
|
|
interface BrandRow {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
seo_title: string | null;
|
|
seo_description: string | null;
|
|
created_at: Date;
|
|
updated_at: Date;
|
|
}
|
|
|
|
const UNIQUE_VIOLATION = '23505';
|
|
|
|
const UPDATABLE: ReadonlyArray<[keyof BrandPatch, string]> = [
|
|
['name', 'name'],
|
|
['slug', 'slug'],
|
|
['seoTitle', 'seo_title'],
|
|
['seoDescription', 'seo_description'],
|
|
];
|
|
|
|
export class PgBrandRepository implements BrandRepository {
|
|
constructor(private readonly pool: pg.Pool) {}
|
|
|
|
async findById(id: string): Promise<Brand | undefined> {
|
|
const result = await this.pool.query<BrandRow>('SELECT * FROM brands_brands WHERE id = $1', [
|
|
id,
|
|
]);
|
|
const row = result.rows[0];
|
|
return row ? toBrand(row) : undefined;
|
|
}
|
|
|
|
async findBySlug(slug: string): Promise<Brand | undefined> {
|
|
const result = await this.pool.query<BrandRow>('SELECT * FROM brands_brands WHERE slug = $1', [
|
|
slug,
|
|
]);
|
|
const row = result.rows[0];
|
|
return row ? toBrand(row) : undefined;
|
|
}
|
|
|
|
async list(): Promise<Brand[]> {
|
|
const result = await this.pool.query<BrandRow>(
|
|
'SELECT * FROM brands_brands ORDER BY name ASC, id ASC',
|
|
);
|
|
return result.rows.map(toBrand);
|
|
}
|
|
|
|
async create(input: NewBrand): Promise<Brand> {
|
|
try {
|
|
const result = await this.pool.query<BrandRow>(
|
|
`INSERT INTO brands_brands (name, slug, seo_title, seo_description)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *`,
|
|
[input.name, input.slug, input.seoTitle ?? null, input.seoDescription ?? null],
|
|
);
|
|
const row = result.rows[0];
|
|
if (!row) {
|
|
throw new Error('brands_brands INSERT returned no row');
|
|
}
|
|
return toBrand(row);
|
|
} catch (error) {
|
|
if (isPgError(error, UNIQUE_VIOLATION)) {
|
|
throw new BrandSlugAlreadyExistsError();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async update(id: string, patch: BrandPatch): Promise<Brand | undefined> {
|
|
const setClauses: string[] = [];
|
|
const values: unknown[] = [];
|
|
for (const [key, column] of UPDATABLE) {
|
|
if (key in patch) {
|
|
values.push(patch[key]);
|
|
setClauses.push(`${column} = $${values.length}`);
|
|
}
|
|
}
|
|
if (setClauses.length === 0) {
|
|
return this.findById(id);
|
|
}
|
|
|
|
values.push(id);
|
|
try {
|
|
const result = await this.pool.query<BrandRow>(
|
|
`UPDATE brands_brands SET ${setClauses.join(', ')}, updated_at = now()
|
|
WHERE id = $${values.length}
|
|
RETURNING *`,
|
|
values,
|
|
);
|
|
const row = result.rows[0];
|
|
return row ? toBrand(row) : undefined;
|
|
} catch (error) {
|
|
if (isPgError(error, UNIQUE_VIOLATION)) {
|
|
throw new BrandSlugAlreadyExistsError();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async delete(id: string): Promise<void> {
|
|
await this.pool.query('DELETE FROM brands_brands WHERE id = $1', [id]);
|
|
}
|
|
}
|
|
|
|
function toBrand(row: BrandRow): Brand {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
slug: row.slug,
|
|
seoTitle: row.seo_title,
|
|
seoDescription: row.seo_description,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
};
|
|
}
|
|
|
|
function isPgError(error: unknown, code: string): boolean {
|
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
}
|