init
This commit is contained in:
128
src/app/api/products/[id]/route.ts
Normal file
128
src/app/api/products/[id]/route.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { and, eq, type InferSelectModel } from 'drizzle-orm';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { products } from '@/db/schema';
|
||||
import type { ProductMutationPayload } from '@/features/products/api/types';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
function toProductResponse(product: InferSelectModel<typeof products>) {
|
||||
return {
|
||||
id: product.id,
|
||||
organization_id: product.organizationId,
|
||||
photo_url: product.photoUrl,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
created_at: product.createdAt.toISOString(),
|
||||
price: product.price,
|
||||
category: product.category,
|
||||
updated_at: product.updatedAt.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess();
|
||||
const { id } = await params;
|
||||
|
||||
const product = await db.query.products.findFirst({
|
||||
where: and(eq(products.id, Number(id)), eq(products.organizationId, organization.id))
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: `Product with ID ${id} not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: `Product with ID ${id} found`,
|
||||
product: toProductResponse(product)
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load product' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess();
|
||||
const { id } = await params;
|
||||
const body = (await request.json()) as ProductMutationPayload;
|
||||
|
||||
const existingProduct = await db.query.products.findFirst({
|
||||
where: and(eq(products.id, Number(id)), eq(products.organizationId, organization.id))
|
||||
});
|
||||
|
||||
if (!existingProduct) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: `Product with ID ${id} not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const [product] = await db
|
||||
.update(products)
|
||||
.set({
|
||||
name: body.name,
|
||||
category: body.category,
|
||||
price: body.price,
|
||||
description: body.description,
|
||||
photoUrl: body.photoDataUrl || existingProduct.photoUrl,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(and(eq(products.id, Number(id)), eq(products.organizationId, organization.id)))
|
||||
.returning();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Product updated successfully',
|
||||
product: toProductResponse(product)
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to update product' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess();
|
||||
const { id } = await params;
|
||||
|
||||
const [deletedProduct] = await db
|
||||
.delete(products)
|
||||
.where(and(eq(products.id, Number(id)), eq(products.organizationId, organization.id)))
|
||||
.returning({ id: products.id });
|
||||
|
||||
if (!deletedProduct) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: `Product with ID ${id} not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Product deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to delete product' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
146
src/app/api/products/route.ts
Normal file
146
src/app/api/products/route.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { and, asc, count, desc, eq, ilike, inArray, or } from 'drizzle-orm';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { products } from '@/db/schema';
|
||||
import type { ProductMutationPayload } from '@/features/products/api/types';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
const sortMap = {
|
||||
name: products.name,
|
||||
category: products.category,
|
||||
price: products.price,
|
||||
created_at: products.createdAt,
|
||||
updated_at: products.updatedAt
|
||||
} as const;
|
||||
|
||||
function buildProductImageUrl(name: string) {
|
||||
return `https://placehold.co/120x120/336b4a/ffffff/png?text=${encodeURIComponent(name.slice(0, 12))}`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess();
|
||||
const { searchParams } = request.nextUrl;
|
||||
|
||||
const page = Number(searchParams.get('page') ?? 1);
|
||||
const limit = Number(searchParams.get('limit') ?? 10);
|
||||
const categories = searchParams.get('categories');
|
||||
const search = searchParams.get('search');
|
||||
const sort = searchParams.get('sort');
|
||||
const categoryValues = categories
|
||||
? categories
|
||||
.split(/[.,]/)
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const filters = [
|
||||
eq(products.organizationId, organization.id),
|
||||
...(categoryValues.length ? [inArray(products.category, categoryValues)] : []),
|
||||
...(search
|
||||
? [
|
||||
or(
|
||||
ilike(products.name, `%${search}%`),
|
||||
ilike(products.description, `%${search}%`),
|
||||
ilike(products.category, `%${search}%`)
|
||||
)!
|
||||
]
|
||||
: [])
|
||||
];
|
||||
|
||||
const where = filters.length === 1 ? filters[0] : and(...filters);
|
||||
|
||||
const [totalResult] = await db.select({ value: count() }).from(products).where(where);
|
||||
|
||||
let orderByClause = desc(products.createdAt);
|
||||
|
||||
if (sort) {
|
||||
try {
|
||||
const sortItems = JSON.parse(sort) as Array<{ id: keyof typeof sortMap; desc: boolean }>;
|
||||
const primarySort = sortItems[0];
|
||||
|
||||
if (primarySort?.id && sortMap[primarySort.id]) {
|
||||
orderByClause = primarySort.desc
|
||||
? desc(sortMap[primarySort.id])
|
||||
: asc(sortMap[primarySort.id]);
|
||||
}
|
||||
} catch {
|
||||
orderByClause = desc(products.createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
const rows = await db.select().from(products).where(where).orderBy(orderByClause).limit(limit).offset(offset);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Products loaded successfully',
|
||||
total_products: totalResult?.value ?? 0,
|
||||
offset,
|
||||
limit,
|
||||
products: rows.map((product) => ({
|
||||
id: product.id,
|
||||
organization_id: product.organizationId,
|
||||
photo_url: product.photoUrl,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
created_at: product.createdAt.toISOString(),
|
||||
price: product.price,
|
||||
category: product.category,
|
||||
updated_at: product.updatedAt.toISOString()
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load products' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess();
|
||||
const body = (await request.json()) as ProductMutationPayload;
|
||||
|
||||
const [product] = await db
|
||||
.insert(products)
|
||||
.values({
|
||||
organizationId: organization.id,
|
||||
name: body.name,
|
||||
category: body.category,
|
||||
price: body.price,
|
||||
description: body.description,
|
||||
photoUrl: body.photoDataUrl || buildProductImageUrl(body.name)
|
||||
})
|
||||
.returning();
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Product created successfully',
|
||||
product: {
|
||||
id: product.id,
|
||||
organization_id: product.organizationId,
|
||||
photo_url: product.photoUrl,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
created_at: product.createdAt.toISOString(),
|
||||
price: product.price,
|
||||
category: product.category,
|
||||
updated_at: product.updatedAt.toISOString()
|
||||
}
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to create product' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user