feat(db): add database repositories

This commit is contained in:
MastersGame
2026-11-05 23:00:48 +01:00
parent aa4262c45a
commit 60cd4cc1d6
6 changed files with 232 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Brand } from '../../prisma/generated/client';
import { BrandCreateInput } from '../../prisma/generated/models/Brand';
/** Database access to brands. Rows are matched with Hiboutik by `hiboutikId`. */
@Injectable()
export class BrandDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
async getAll(): Promise<Brand[]> {
return this.prisma.brand.findMany({
orderBy: { hiboutikId: 'asc' },
});
}
/** Creates or updates the brand with the same `hiboutikId`. */
async upsert(brand: BrandCreateInput): Promise<Brand> {
return this.prisma.brand.upsert({
where: { hiboutikId: brand.hiboutikId },
create: brand,
update: brand,
});
}
/**
* Deletes the brands that no longer exist in Hiboutik.
* Their products are kept: the relation is set to null (onDelete: SetNull).
*/
async deleteMissing(keptHiboutikIds: number[]): Promise<number> {
const result = await this.prisma.brand.deleteMany({
where: { hiboutikId: { notIn: keptHiboutikIds } },
});
return result.count;
}
}

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Category } from '../../prisma/generated/client';
import { CategoryCreateInput } from '../../prisma/generated/models/Category';
/** Database access to categories. Rows are matched with Hiboutik by `hiboutikId`. */
@Injectable()
export class CategoryDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
async getAll(): Promise<Category[]> {
return this.prisma.category.findMany({
orderBy: { hiboutikId: 'asc' },
});
}
/** Creates or updates the category with the same `hiboutikId`. */
async upsert(category: CategoryCreateInput): Promise<Category> {
return this.prisma.category.upsert({
where: { hiboutikId: category.hiboutikId },
create: category,
update: category,
});
}
/** Links a category to its parent (null for a root category). Both must already exist. */
async setParent(hiboutikId: number, parentHiboutikId: number | null): Promise<Category> {
return this.prisma.category.update({
where: { hiboutikId },
data: { parent: parentHiboutikId ? { connect: { hiboutikId: parentHiboutikId } } : { disconnect: true } },
});
}
/**
* Deletes the categories that no longer exist in Hiboutik.
* Their products and sub-categories are kept: the relation is set to null (onDelete: SetNull).
*/
async deleteMissing(keptHiboutikIds: number[]): Promise<number> {
const result = await this.prisma.category.deleteMany({
where: { hiboutikId: { notIn: keptHiboutikIds } },
});
return result.count;
}
}

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Customer } from '../../prisma/generated/client';
import { CustomerCreateInput } from '../../prisma/generated/models/Customer';
/** Database access to customers. Rows are matched with Hiboutik by `hiboutikId`. */
@Injectable()
export class CustomerDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
async getAll(): Promise<Customer[]> {
return this.prisma.customer.findMany({
orderBy: { hiboutikId: 'asc' },
});
}
/** Creates or updates the customer with the same `hiboutikId`. */
async upsert(customer: CustomerCreateInput): Promise<Customer> {
return this.prisma.customer.upsert({
where: { hiboutikId: customer.hiboutikId },
create: customer,
update: customer,
});
}
/**
* Deletes the customers that no longer exist in Hiboutik.
* Their addresses are deleted (onDelete: Cascade), their orders are kept with no customer (onDelete: SetNull).
*/
async deleteMissing(keptHiboutikIds: number[]): Promise<number> {
const result = await this.prisma.customer.deleteMany({
where: { hiboutikId: { notIn: keptHiboutikIds } },
});
return result.count;
}
}

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Product } from '../../prisma/generated/client';
import { ProductCreateInput } from '../../prisma/generated/models/Product';
/** Database access to products. Rows are matched with Hiboutik by `hiboutikId`. */
@Injectable()
export class ProductDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
async getAll(): Promise<Product[]> {
return this.prisma.product.findMany({
orderBy: { hiboutikId: 'asc' },
});
}
/** Creates or updates the product with the same `hiboutikId`. On update, the relations missing from the input are unlinked. */
async upsert(product: ProductCreateInput): Promise<Product> {
return this.prisma.product.upsert({
where: { hiboutikId: product.hiboutikId },
create: product,
update: {
...product,
// an undefined relation is ignored by Prisma on update: explicitly unlink relations removed in Hiboutik
brand: product.brand ?? { disconnect: true },
category: product.category ?? { disconnect: true },
supplier: product.supplier ?? { disconnect: true },
},
});
}
/**
* Deletes products (and their versions, by cascade).
* Order lines keep their own snapshot of the product, their link is set to null.
*/
async deleteAll(productIds: string[]): Promise<number> {
if (productIds.length === 0) return 0;
const result = await this.prisma.product.deleteMany({
where: { id: { in: productIds } },
});
return result.count;
}
}

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { ProductVersion } from '../../prisma/generated/client';
import { ProductVersionCreateInput } from '../../prisma/generated/models/ProductVersion';
/** Database access to product versions (one per Hiboutik size), identified by their product and `hiboutikSizeId`. */
@Injectable()
export class ProductVersionDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
async getAll(): Promise<ProductVersion[]> {
return this.prisma.productVersion.findMany({
orderBy: { hiboutikSizeId: 'asc' },
});
}
/** Creates or updates the version of `productId` with the same `hiboutikSizeId`. */
async upsert(productId: string, productVersion: ProductVersionCreateInput): Promise<ProductVersion> {
return this.prisma.productVersion.upsert({
where: { productId_hiboutikSizeId: { productId: productId, hiboutikSizeId: productVersion.hiboutikSizeId } },
create: productVersion,
update: productVersion,
});
}
/**
* Deletes the versions of a product that no longer exist in Hiboutik.
* Order lines keep their own snapshot of the product, their link is set to null.
*/
async deleteMissing(productId: string, keptHiboutikSizeIds: number[]): Promise<number> {
const result = await this.prisma.productVersion.deleteMany({
where: { productId, hiboutikSizeId: { notIn: keptHiboutikSizeIds } },
});
return result.count;
}
}

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Supplier } from '../../prisma/generated/client';
import { SupplierCreateInput } from '../../prisma/generated/models/Supplier';
/** Database access to suppliers. Rows are matched with Hiboutik by `hiboutikId`. */
@Injectable()
export class SupplierDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
async getAll(): Promise<Supplier[]> {
return this.prisma.supplier.findMany({
orderBy: { hiboutikId: 'asc' },
});
}
/** Creates or updates the supplier with the same `hiboutikId`. */
async upsert(supplier: SupplierCreateInput): Promise<Supplier> {
return this.prisma.supplier.upsert({
where: { hiboutikId: supplier.hiboutikId },
create: supplier,
update: supplier,
});
}
/**
* Deletes the suppliers that no longer exist in Hiboutik.
* Their products are kept: the relation is set to null (onDelete: SetNull).
*/
async deleteMissing(keptHiboutikIds: number[]): Promise<number> {
const result = await this.prisma.supplier.deleteMany({
where: { hiboutikId: { notIn: keptHiboutikIds } },
});
return result.count;
}
}