Files
the-pet-loft/convex/categories.ts
ianshaloom cc15338ad9 feat: initial commit — storefront, convex backend, and shared packages
Completes the first milestone of The Pet Loft ecommerce platform:
- apps/storefront: full customer-facing Next.js app with HeroUI (cart,
  checkout, orders, wishlist, product detail, shop, search, auth)
- convex/: serverless backend with schema, queries, mutations, actions,
  HTTP routes, Stripe/Shippo integrations, and co-located tests
- packages/types, packages/utils, packages/convex: shared workspace packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:31:18 +03:00

121 lines
3.3 KiB
TypeScript

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import * as Users from "./model/users";
import * as Categories from "./model/categories";
export const list = query({
args: {
parentId: v.optional(v.id("categories")),
},
handler: async (ctx, args) => {
let items;
console.log("args in list", args);
if (args.parentId !== undefined) {
items = await ctx.db
.query("categories")
.withIndex("by_parent", (q) => q.eq("parentId", args.parentId!))
.collect();
} else {
items = await ctx.db.query("categories").collect();
}
items.sort((a, b) => a.name.localeCompare(b.name));
return items;
},
});
export const getById = query({
args: { id: v.id("categories") },
handler: async (ctx, { id }) => {
return await ctx.db.get(id);
},
});
export const getBySlug = query({
args: { slug: v.string() },
handler: async (ctx, { slug }) => {
return await ctx.db
.query("categories")
.withIndex("by_slug", (q) => q.eq("slug", slug))
.unique();
},
});
export const getByPath = query({
args: {
categorySlug: v.string(),
subCategorySlug: v.string(),
},
handler: async (ctx, { categorySlug, subCategorySlug }) => {
const parent = await ctx.db
.query("categories")
.withIndex("by_slug", (q) => q.eq("slug", categorySlug))
.unique();
if (!parent) return null;
return await ctx.db
.query("categories")
.withIndex("by_parent_slug", (q) =>
q.eq("parentId", parent._id).eq("slug", subCategorySlug),
)
.unique();
},
});
export const listByTopCategory = query({
args: { slug: v.string() },
handler: async (ctx, { slug }) => {
const items = await ctx.db
.query("categories")
.withIndex("by_top_category_slug", (q) =>
q.eq("topCategorySlug", slug),
)
.collect();
items.sort((a, b) => a.name.localeCompare(b.name));
return items;
},
});
export const create = mutation({
args: {
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
parentId: v.optional(v.id("categories")),
topCategorySlug: v.optional(v.string()),
seoTitle: v.optional(v.string()),
seoDescription: v.optional(v.string()),
},
handler: async (ctx, args) => {
await Users.requireAdmin(ctx);
const existing = await ctx.db
.query("categories")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.unique();
if (existing) throw new Error("Category slug already exists");
return await ctx.db.insert("categories", args);
},
});
export const update = mutation({
args: {
id: v.id("categories"),
name: v.optional(v.string()),
slug: v.optional(v.string()),
description: v.optional(v.string()),
topCategorySlug: v.optional(v.string()),
seoTitle: v.optional(v.string()),
seoDescription: v.optional(v.string()),
},
handler: async (ctx, { id, ...updates }) => {
await Users.requireAdmin(ctx);
await Categories.getCategoryOrThrow(ctx, id);
const fields: Record<string, unknown> = {};
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) fields[key] = value;
}
if (Object.keys(fields).length > 0) {
await ctx.db.patch(id, fields);
}
return id;
},
});