product admin crud

This commit is contained in:
user
2025-10-20 19:16:19 +03:00
parent e239b3bbf6
commit 2cc0ca4c51
22 changed files with 2601 additions and 35 deletions

View File

@@ -1,14 +1,37 @@
import { z } from "zod";
export const productModel = z.object({
id: z.string(),
linkId: z.string(),
title: z.string(),
description: z.string(),
longDescription: z.string(),
price: z.number(),
discountPrice: z.number(),
id: z.number().optional(),
linkId: z.string().min(8).max(32),
title: z.string().min(1).max(64),
description: z.string().min(1),
longDescription: z.string().min(1),
price: z.coerce.number().nonnegative(),
discountPrice: z.coerce.number().nonnegative(),
createdAt: z.coerce.string().optional(),
updatedAt: z.coerce.string().optional(),
});
export type ProductModel = z.infer<typeof productModel>;
export const createProductPayload = productModel.omit({
id: true,
linkId: true,
createdAt: true,
updatedAt: true,
});
export type CreateProductPayload = z.infer<typeof createProductPayload>;
export const updateProductPayload = productModel
.omit({
linkId: true,
createdAt: true,
updatedAt: true,
})
.partial()
.extend({
id: z.number(),
});
export type UpdateProductPayload = z.infer<typeof updateProductPayload>;

View File

@@ -1,4 +1,14 @@
import { Database } from "@pkg/db";
import { desc, eq, type Database } from "@pkg/db";
import { product } from "@pkg/db/schema";
import { getError, Logger } from "@pkg/logger";
import { ERROR_CODES, type Result } from "@pkg/result";
import { nanoid } from "nanoid";
import {
productModel,
type CreateProductPayload,
type ProductModel,
type UpdateProductPayload,
} from "./data";
export class ProductRepository {
private db: Database;
@@ -7,9 +17,277 @@ export class ProductRepository {
this.db = db;
}
// TODO: compelte the crud method implementation
async listAllProducts() {}
async createProduct() {}
async updateProduct() {}
async deleteProduct() {}
async listAllProducts(): Promise<Result<ProductModel[]>> {
try {
const results = await this.db.query.product.findMany({
orderBy: [desc(product.createdAt)],
});
const out = [] as ProductModel[];
for (const result of results) {
const parsed = productModel.safeParse(result);
if (!parsed.success) {
Logger.error("Failed to parse product");
Logger.error(parsed.error);
continue;
}
out.push(parsed.data);
}
return { data: out };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to fetch products",
detail:
"An error occurred while retrieving products from the database",
userHint: "Please try refreshing the page",
actionable: false,
},
e,
),
};
}
}
async getProductById(id: number): Promise<Result<ProductModel>> {
try {
const result = await this.db.query.product.findFirst({
where: eq(product.id, id),
});
if (!result) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Product not found",
detail: "No product exists with the provided ID",
userHint: "Please check the product ID and try again",
actionable: true,
}),
};
}
const parsed = productModel.safeParse(result);
if (!parsed.success) {
Logger.error("Failed to parse product", result);
return {
error: getError({
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
message: "Failed to parse product",
userHint: "Please try again",
detail: "Failed to parse product",
}),
};
}
return { data: parsed.data };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to fetch product",
detail:
"An error occurred while retrieving the product from the database",
userHint: "Please try refreshing the page",
actionable: false,
},
e,
),
};
}
}
async createProduct(payload: CreateProductPayload): Promise<Result<number>> {
try {
// Generate a unique linkId
const linkId = nanoid(16);
const result = await this.db
.insert(product)
.values({
linkId,
title: payload.title,
description: payload.description,
longDescription: payload.longDescription,
price: payload.price.toString(),
discountPrice: payload.discountPrice.toString(),
})
.returning({ id: product.id })
.execute();
if (!result || result.length === 0) {
throw new Error("Failed to create product record");
}
return { data: result[0].id };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to create product",
detail: "An error occurred while creating the product",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
async updateProduct(payload: UpdateProductPayload): Promise<Result<boolean>> {
try {
if (!payload.id) {
return {
error: getError({
code: ERROR_CODES.VALIDATION_ERROR,
message: "Invalid product ID",
detail: "No product ID was provided for the update operation",
userHint: "Please provide a valid product ID",
actionable: true,
}),
};
}
// Check if product exists
const existing = await this.db.query.product.findFirst({
where: eq(product.id, payload.id),
});
if (!existing) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Product not found",
detail: "No product exists with the provided ID",
userHint: "Please check the product ID and try again",
actionable: true,
}),
};
}
// Build the update object with only the fields that are provided
const updateValues: Record<string, any> = {};
if (payload.title !== undefined) updateValues.title = payload.title;
if (payload.description !== undefined)
updateValues.description = payload.description;
if (payload.longDescription !== undefined)
updateValues.longDescription = payload.longDescription;
if (payload.price !== undefined)
updateValues.price = payload.price.toString();
if (payload.discountPrice !== undefined)
updateValues.discountPrice = payload.discountPrice.toString();
updateValues.updatedAt = new Date();
await this.db
.update(product)
.set(updateValues)
.where(eq(product.id, payload.id))
.execute();
return { data: true };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to update product",
detail: "An error occurred while updating the product",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
async deleteProduct(id: number): Promise<Result<boolean>> {
try {
// Check if product exists
const existing = await this.db.query.product.findFirst({
where: eq(product.id, id),
});
if (!existing) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Product not found",
detail: "No product exists with the provided ID",
userHint: "Please check the product ID and try again",
actionable: true,
}),
};
}
await this.db.delete(product).where(eq(product.id, id)).execute();
return { data: true };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to delete product",
detail: "An error occurred while deleting the product",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
async refreshProductLinkId(id: number): Promise<Result<string>> {
try {
// Check if product exists
const existing = await this.db.query.product.findFirst({
where: eq(product.id, id),
});
if (!existing) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Product not found",
detail: "No product exists with the provided ID",
userHint: "Please check the product ID and try again",
actionable: true,
}),
};
}
// Generate a new unique linkId
const newLinkId = nanoid(16);
await this.db
.update(product)
.set({
linkId: newLinkId,
updatedAt: new Date(),
})
.where(eq(product.id, id))
.execute();
return { data: newLinkId };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to refresh product link ID",
detail: "An error occurred while refreshing the product's link ID",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
}

View File

@@ -0,0 +1,39 @@
import { db } from "@pkg/db";
import type { CreateProductPayload, UpdateProductPayload } from "./data";
import { ProductRepository } from "./repository";
export class ProductUseCases {
private repo: ProductRepository;
constructor(repo: ProductRepository) {
this.repo = repo;
}
async getAllProducts() {
return this.repo.listAllProducts();
}
async getProductById(id: number) {
return this.repo.getProductById(id);
}
async createProduct(payload: CreateProductPayload) {
return this.repo.createProduct(payload);
}
async updateProduct(payload: UpdateProductPayload) {
return this.repo.updateProduct(payload);
}
async deleteProduct(id: number) {
return this.repo.deleteProduct(id);
}
async refreshProductLinkId(id: number) {
return this.repo.refreshProductLinkId(id);
}
}
export function getProductUseCases() {
return new ProductUseCases(new ProductRepository(db));
}