- Introduced a comprehensive markdown document outlining implementation rules for Convex functions, including syntax, registration, HTTP endpoints, and TypeScript usage. - Created a new configuration file for the Convex app, integrating the Resend service. - Added a new HTTP route for handling Shippo webhooks to ensure proper response handling. - Implemented integration tests for order timeline events, covering various scenarios including order fulfillment and status changes. - Enhanced existing functions with type safety improvements and additional validation logic. This commit establishes clear guidelines for backend development and improves the overall structure and reliability of the Convex application.
184 lines
5.4 KiB
TypeScript
184 lines
5.4 KiB
TypeScript
import { query, mutation } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import * as Users from "./model/users";
|
|
import { enrichProducts } from "./model/products";
|
|
import type { Id, Doc } from "./_generated/dataModel";
|
|
|
|
export const list = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const rows = await ctx.db
|
|
.query("wishlists")
|
|
.withIndex("by_user", (q) => q.eq("userId", user._id))
|
|
.collect();
|
|
|
|
if (rows.length === 0) return [];
|
|
|
|
const productIds = [...new Set(rows.map((r) => r.productId))];
|
|
const products = (
|
|
await Promise.all(productIds.map((id) => ctx.db.get(id)))
|
|
).filter((p): p is Doc<"products"> => p != null);
|
|
|
|
const enriched = await enrichProducts(ctx, products);
|
|
const productMap = new Map(
|
|
enriched.map((p) => [p._id, p]),
|
|
);
|
|
|
|
return rows.map((row) => {
|
|
const product = productMap.get(row.productId);
|
|
const variant = row.variantId && product?.variants
|
|
? product.variants.find((v: { _id: Id<"productVariants"> }) => v._id === row.variantId)
|
|
: undefined;
|
|
return { ...row, product, variant };
|
|
});
|
|
},
|
|
});
|
|
|
|
function findExistingEntry(
|
|
rows: { variantId?: Id<"productVariants"> }[],
|
|
variantId?: Id<"productVariants">,
|
|
) {
|
|
return rows.find((r) => {
|
|
if (variantId === undefined && r.variantId === undefined) return true;
|
|
return r.variantId === variantId;
|
|
});
|
|
}
|
|
|
|
export const add = mutation({
|
|
args: {
|
|
productId: v.id("products"),
|
|
variantId: v.optional(v.id("productVariants")),
|
|
notifyOnPriceDrop: v.optional(v.boolean()),
|
|
notifyOnBackInStock: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const existing = await ctx.db
|
|
.query("wishlists")
|
|
.withIndex("by_user_and_product", (q) =>
|
|
q.eq("userId", user._id).eq("productId", args.productId),
|
|
)
|
|
.collect();
|
|
|
|
const found = findExistingEntry(existing, args.variantId);
|
|
if (found)
|
|
return { id: (found as { _id: Id<"wishlists"> })._id, alreadyExisted: true };
|
|
|
|
let priceWhenAdded = 0;
|
|
if (args.variantId) {
|
|
const variant = await ctx.db.get(args.variantId);
|
|
if (variant && variant.productId === args.productId) {
|
|
priceWhenAdded = variant.price;
|
|
}
|
|
} else {
|
|
const variants = await ctx.db
|
|
.query("productVariants")
|
|
.withIndex("by_product_and_active", (q) =>
|
|
q.eq("productId", args.productId).eq("isActive", true),
|
|
)
|
|
.first();
|
|
if (variants) priceWhenAdded = variants.price;
|
|
}
|
|
|
|
const id = await ctx.db.insert("wishlists", {
|
|
userId: user._id,
|
|
productId: args.productId,
|
|
variantId: args.variantId,
|
|
addedAt: Date.now(),
|
|
notifyOnPriceDrop: args.notifyOnPriceDrop ?? false,
|
|
notifyOnBackInStock: args.notifyOnBackInStock ?? false,
|
|
priceWhenAdded,
|
|
});
|
|
return { id, alreadyExisted: false };
|
|
},
|
|
});
|
|
|
|
export const remove = mutation({
|
|
args: { id: v.id("wishlists") },
|
|
handler: async (ctx, { id }) => {
|
|
const doc = await ctx.db.get(id);
|
|
if (!doc) throw new Error("Wishlist item not found");
|
|
await Users.requireOwnership(ctx, doc.userId);
|
|
await ctx.db.delete(id);
|
|
},
|
|
});
|
|
|
|
export const toggle = mutation({
|
|
args: {
|
|
productId: v.id("products"),
|
|
variantId: v.optional(v.id("productVariants")),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const existing = await ctx.db
|
|
.query("wishlists")
|
|
.withIndex("by_user_and_product", (q) =>
|
|
q.eq("userId", user._id).eq("productId", args.productId),
|
|
)
|
|
.collect();
|
|
|
|
const found = findExistingEntry(existing, args.variantId) as
|
|
| { _id: Id<"wishlists"> }
|
|
| undefined;
|
|
if (found) {
|
|
await ctx.db.delete(found._id);
|
|
return { removed: true };
|
|
}
|
|
|
|
const id = await ctx.db.insert("wishlists", {
|
|
userId: user._id,
|
|
productId: args.productId,
|
|
variantId: args.variantId,
|
|
addedAt: Date.now(),
|
|
notifyOnPriceDrop: false,
|
|
notifyOnBackInStock: false,
|
|
priceWhenAdded: await (async () => {
|
|
if (args.variantId) {
|
|
const v = await ctx.db.get(args.variantId);
|
|
return v && v.productId === args.productId ? v.price : 0;
|
|
}
|
|
const first = await ctx.db
|
|
.query("productVariants")
|
|
.withIndex("by_product_and_active", (q) =>
|
|
q.eq("productId", args.productId).eq("isActive", true),
|
|
)
|
|
.first();
|
|
return first?.price ?? 0;
|
|
})(),
|
|
});
|
|
return { added: true, id };
|
|
},
|
|
});
|
|
|
|
export const count = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const user = await Users.getCurrentUser(ctx);
|
|
if (!user) return 0;
|
|
|
|
const rows = await ctx.db
|
|
.query("wishlists")
|
|
.withIndex("by_user", (q) => q.eq("userId", user._id))
|
|
.collect();
|
|
return rows.length;
|
|
},
|
|
});
|
|
|
|
export const isWishlisted = query({
|
|
args: {
|
|
productId: v.id("products"),
|
|
variantId: v.optional(v.id("productVariants")),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const rows = await ctx.db
|
|
.query("wishlists")
|
|
.withIndex("by_user_and_product", (q) =>
|
|
q.eq("userId", user._id).eq("productId", args.productId),
|
|
)
|
|
.collect();
|
|
return !!findExistingEntry(rows, args.variantId);
|
|
},
|
|
});
|