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>
121 lines
3.3 KiB
TypeScript
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;
|
|
},
|
|
});
|