ProjectR Docs
Backend (Convex)

API — productEngagement

Generated catalog of Convex functions in convex/productEngagement/. Regenerated by `bun run docs:api`.

Auto-generated from source. Do not edit by hand — run bun run docs:api.

query (24)

FunctionDescriptionArgs
getAllBlocksGet all user block relationships (admin-only, paginated){ limit: v.optional(v.number()), offset: v.optional(v.number()), }
getAllRatingsGet all ratings with filters and pagination (Dashboard) Used for managing product reviews/ratings in admin panel{ status: v.optional( v.union( v.literal("PENDING"), v.literal("APPROVED"), v.literal("REJECTED"), v.literal("FLAGGED") ) ), productId: v.optional(v.id("shop…
getAllReportsGet all user reports across all reviews (admin-only, paginated) Unlike getCommentReports which is per-comment, this provides a global view.{ status: v.optional( v.union(v.literal("pending"), v.literal("reviewed"), v.literal("dismissed")) ), limit: v.optional(v.number()), offset: v.optional(v.num…
getBasedOnPurchasesGet recommendations based on purchase history{ limit: v.optional(v.number()), }
getBlockedUsersGet list of blocked users with their profile names.{}
getCommentReportsGet reports for a specific comment (dashboard admin-only).{ commentId: v.id("productComments"), }
getFlaggedRatingsGet flagged ratings for review (Dashboard){ limit: v.optional(v.number()), offset: v.optional(v.number()), }
getListProductsPaginatedProducts in a list, for Convex usePaginatedQuery (mobile wishlist infinite scroll). Cursor-driven via .paginate over the by_list_sort index (sortOrder ascending), so already-loaded pages stay reactive and each page is read once — replaces the mobile expanding-limit pattern that re-read the whole window on every load-more. Product details are joined per page item. totalCount is intentionally omitted — mobile reads the list size from getUserWishlists.productCount; paging state comes from isDone.{ listId: v.id("userProductLists"), paginationOpts: paginationOptsValidator, }
getPendingRatingsGet pending ratings awaiting moderation (Dashboard){ limit: v.optional(v.number()), offset: v.optional(v.number()), }
getPersonalizedRecommendationsGet personalized recommendations for user Based on: purchase history, views, saved items, beauty profile{ limit: v.optional(v.number()), excludeOwned: v.optional(v.boolean()), }
getProductComments—{ productId: v.string(), status: v.optional( v.union( v.literal("PENDING"), v.literal("APPROVED"), v.literal("REJECTED"), v.literal("FLAGGED") ) ), limit: v.…
getProductEngagementAnalyticsGet product engagement analytics (admin){ productId: v.optional(v.id("shopifyProducts")), dateFrom: v.number(), dateTo: v.number(), limit: v.optional(v.number()), }
getProductInteractionStatsGet interaction statistics for a product{ productId: v.id("shopifyProducts"), }
getRatingGet a single rating by ID (Dashboard){ id: v.id("productComments"), }
getRatingAnalyticsGet detailed rating analytics for dashboard{ productId: v.optional(v.id("shopifyProducts")), dateFrom: v.number(), dateTo: v.number(), }
getRecentlyViewedGet recently viewed products for user{ limit: v.optional(v.number()), }
getTrendingProductsGet trending products based on recent interactions{ limit: v.optional(v.number()), timeWindowDays: v.optional(v.number()), }
getUserCommentsGet comments by current user{ limit: v.optional(v.number()), status: v.optional( v.union( v.literal("PENDING"), v.literal("APPROVED"), v.literal("REJECTED"), v.literal("FLAGGED") ) ), }
getUserCommentVoteCheck if current user has voted on a comment{ commentId: v.id("productComments"), }
getUserEngagementAnalyticsGet user engagement analytics{ userId: v.optional(v.string()), dateFrom: v.optional(v.number()), dateTo: v.optional(v.number()), }
getUserProductReviewGet current user's review for a specific product (any status) Used by mobile to detect existing reviews regardless of moderation status. Returns same shape as getProductComments items for type compatibility.{ productId: v.string() }
getUserRecentInteractionsGet user's recent product interactions{ interactionType: v.optional( v.union( v.literal("VIEW"), v.literal("SAVE"), v.literal("SHARE"), v.literal("CART_ADD"), v.literal("PURCHASE"), v.literal("QU…
getUserWishlistsGet user's product lists (wishlists, favorites, etc.){ type: v.optional( v.union( v.literal("WISHLIST"), v.literal("FAVORITES"), v.literal("COLLECTION"), v.literal("COMPARISON") ) ), }
getWishlistedShopifyIdsGet the Shopify GIDs of products in the user's DEFAULT wishlist. Returns the Shopify product id (shopifyId, e.g. "gid://shopify/Product/…"), which is the SAME id every mobile surface already carries — product cards (shop grid, search, home carousels, recently-viewed) and the PDP. Keying the wishlist on the GID means the heart works uniformly everywhere without threading a Convex _id through the product feeds. Lightweight membership-set read: subscribed ONCE at a grid/screen level so every card does an O(1) Set lookup instead of one subscription per card. Reactive — when toggleWishlist mutates, this set updates and all hearts reconcile. Signed-out (or no wishlist yet) returns [].{}

mutation (25)

FunctionDescriptionArgs
addToWishlistAdd a product to a list{ listId: v.id("userProductLists"), productId: v.id("shopifyProducts"), notes: v.optional(v.string()), }
blockUserBlock a user — their reviews will be hidden from the blocker's view.{ blockedUserId: v.string(), }
bulkDeleteCommentsBulk delete comments (admin-only, no ownership check). Runs in a single transaction so the shared comments:<productId> counter and the productRatings aggregate are updated without OCC contention (the previous dashboard path fired N parallel deleteComment calls, which both threw on the ownership check and conflicted on the counter). Reports per-id outcome.{ commentIds: v.array(v.id("productComments")), }
bulkSetCommentStatusBulk set comment moderation status (admin-only). Supports all four statuses (PENDING/APPROVED/REJECTED/FLAGGED) so the dashboard's "Flag" and "Mark as Pending" bulk actions work. Maintains the comment counter + productRatings aggregate on every APPROVED transition.{ commentIds: v.array(v.id("productComments")), status: COMMENT_STATUS, reason: v.optional(v.string()), }
createCommentCreate a new product comment/review Auto-approves ratings-only, requires moderation for comments with content{ productId: v.id("shopifyProducts"), rating: v.optional(v.number()), title: v.optional(v.string()), content: v.string(), images: v.optional(v.array(v.string…
createWishlistCreate a new product list (wishlist, favorites, collection, comparison){ name: v.string(), description: v.optional(v.string()), type: v.union( v.literal("WISHLIST"), v.literal("FAVORITES"), v.literal("COLLECTION"), v.literal("CO…
deleteCommentDelete a comment Only owner can delete their own comment{ commentId: v.id("productComments"), }
deleteReviewImageDelete a review image{ commentId: v.id("productComments"), imageUrl: v.string(), }
deleteWishlistDelete a product list{ listId: v.id("userProductLists"), }
moderateCommentModerate a comment (approve/reject/flag) Admin only{ commentId: v.id("productComments"), status: v.union( v.literal("APPROVED"), v.literal("REJECTED"), v.literal("FLAGGED") ), moderationReason: v.optional(v.s…
recordProductViewRecord a product view{ productId: v.id("shopifyProducts"), sessionId: v.optional(v.string()), deviceInfo: v.optional(v.any()), durationSeconds: v.optional(v.number()), }
removeFromWishlistRemove a product from a list{ listId: v.id("userProductLists"), productId: v.id("shopifyProducts"), }
reorderListItemsReorder items in a list{ listId: v.id("userProductLists"), itemOrder: v.array( v.object({ itemId: v.id("listProducts"), sortOrder: v.number(), }) ), }
reportCommentReport a comment for inappropriate content. Creates an audit trail in userReports and prevents duplicate reports.{ commentId: v.id("productComments"), reason: v.union( v.literal("SPAM"), v.literal("INAPPROPRIATE"), v.literal("FAKE"), v.literal("HARASSMENT"), v.literal("…
resolveReportResolve a user report (mark as reviewed or dismissed){ reportId: v.id("userReports"), resolution: v.union(v.literal("reviewed"), v.literal("dismissed")), }
toggleFavoriteToggle favorite status (add/remove from favorites){ productId: v.id("shopifyProducts"), }
toggleFeaturedCommentFeature/unfeature a comment Admin only{ commentId: v.id("productComments"), isFeatured: v.boolean(), }
trackCartAddTrack cart add{ productId: v.id("shopifyProducts"), variantId: v.optional(v.string()), quantity: v.optional(v.number()), }
trackProductShareTrack product share{ productId: v.id("shopifyProducts"), platform: v.optional(v.string()), }
unblockUserUnblock a user.{ blockedUserId: v.string(), }
updateCommentUpdate an existing comment Only owner can update, resets to pending if content changes{ commentId: v.id("productComments"), rating: v.optional(v.number()), title: v.optional(v.string()), content: v.optional(v.string()), images: v.optional(v.ar…
updateListProductNotesUpdate notes for a product in a list{ listId: v.id("userProductLists"), productId: v.id("shopifyProducts"), notes: v.string(), }
updateWishlistUpdate a product list{ listId: v.id("userProductLists"), name: v.optional(v.string()), description: v.optional(v.string()), isPrivate: v.optional(v.boolean()), sortOrder: v.optio…
uploadReviewImagesUpload review images (returns storage URL) Note: Actual file upload handled by Convex storage{ commentId: v.id("productComments"), imageUrls: v.array(v.string()), }
voteCommentVote on a comment (helpful/not helpful){ commentId: v.id("productComments"), isHelpful: v.boolean(), }

internalMutation (2)

FunctionDescriptionArgs
markProductPurchasedMark product as purchased (internal) Called after order is placed{ userId: v.string(), productId: v.id("shopifyProducts"), orderId: v.optional(v.string()), }
quickAddToWishlistInternalQuick add to wishlist (internal - called by wishlistToggle.ts){ userId: v.string(), productId: v.string(), }

On this page