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>
141 lines
3.5 KiB
TypeScript
141 lines
3.5 KiB
TypeScript
import {
|
|
mutation,
|
|
query,
|
|
internalMutation,
|
|
internalQuery,
|
|
} from "./_generated/server";
|
|
import { paginationOptsValidator } from "convex/server";
|
|
import { v } from "convex/values";
|
|
import * as Users from "./model/users";
|
|
|
|
export const store = mutation({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) throw new Error("Unauthenticated");
|
|
|
|
const existing = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_external_id", (q) =>
|
|
q.eq("externalId", identity.subject),
|
|
)
|
|
.unique();
|
|
|
|
if (existing) {
|
|
if (existing.name !== identity.name) {
|
|
await ctx.db.patch(existing._id, {
|
|
name: identity.name ?? existing.name,
|
|
});
|
|
}
|
|
return existing._id;
|
|
}
|
|
|
|
return await ctx.db.insert("users", {
|
|
name: identity.name ?? "Anonymous",
|
|
email: identity.email ?? "",
|
|
role: "customer",
|
|
externalId: identity.subject,
|
|
avatarUrl: identity.pictureUrl ?? undefined,
|
|
});
|
|
},
|
|
});
|
|
|
|
export const current = query({
|
|
args: {},
|
|
handler: async (ctx) => Users.getCurrentUser(ctx),
|
|
});
|
|
|
|
export const updateProfile = mutation({
|
|
args: {
|
|
name: v.optional(v.string()),
|
|
phone: v.optional(v.string()),
|
|
avatarUrl: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const patch: { name?: string; phone?: string; avatarUrl?: string } = {};
|
|
if (args.name !== undefined) patch.name = args.name;
|
|
if (args.phone !== undefined) patch.phone = args.phone;
|
|
if (args.avatarUrl !== undefined) patch.avatarUrl = args.avatarUrl;
|
|
if (Object.keys(patch).length === 0) return user._id;
|
|
await ctx.db.patch(user._id, patch);
|
|
return user._id;
|
|
},
|
|
});
|
|
|
|
export const listCustomers = query({
|
|
args: {
|
|
paginationOpts: paginationOptsValidator,
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await Users.requireAdmin(ctx);
|
|
return await ctx.db
|
|
.query("users")
|
|
.withIndex("by_role", (q) => q.eq("role", "customer"))
|
|
.order("desc")
|
|
.paginate(args.paginationOpts);
|
|
},
|
|
});
|
|
|
|
export const upsertFromClerk = internalMutation({
|
|
args: {
|
|
externalId: v.string(),
|
|
name: v.string(),
|
|
email: v.string(),
|
|
avatarUrl: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const existing = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_external_id", (q) =>
|
|
q.eq("externalId", args.externalId),
|
|
)
|
|
.unique();
|
|
|
|
if (existing) {
|
|
await ctx.db.patch(existing._id, {
|
|
name: args.name,
|
|
email: args.email,
|
|
avatarUrl: args.avatarUrl,
|
|
});
|
|
} else {
|
|
await ctx.db.insert("users", {
|
|
...args,
|
|
role: "customer",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
export const deleteFromClerk = internalMutation({
|
|
args: { externalId: v.string() },
|
|
handler: async (ctx, { externalId }) => {
|
|
const user = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_external_id", (q) => q.eq("externalId", externalId))
|
|
.unique();
|
|
if (user) await ctx.db.delete(user._id);
|
|
},
|
|
});
|
|
|
|
export const getById = internalQuery({
|
|
args: { userId: v.id("users") },
|
|
handler: async (ctx, args) => {
|
|
const user = await ctx.db.get(args.userId);
|
|
if (!user) throw new Error("User not found");
|
|
return user;
|
|
},
|
|
});
|
|
|
|
export const setStripeCustomerId = internalMutation({
|
|
args: {
|
|
userId: v.id("users"),
|
|
stripeCustomerId: v.string(),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
await ctx.db.patch(args.userId, {
|
|
stripeCustomerId: args.stripeCustomerId,
|
|
});
|
|
},
|
|
});
|