Rocky_Mountain_Vending/lib/convex-service.ts

296 lines
7.1 KiB
TypeScript

import { ConvexHttpClient } from "convex/browser"
import { fetchQuery } from "convex/nextjs"
import { makeFunctionReference } from "convex/server"
import { api } from "@/convex/_generated/api"
import { hasConvexUrl } from "@/lib/convex-config"
import { resolveManualsTenantDomain } from "@/lib/manuals-tenant"
import type { Product } from "@/lib/products/types"
import type { Manual } from "@/lib/manuals-types"
type ConvexProductDoc = {
_id: string
name: string
description?: string
price: number
currency: string
images: string[]
metadata?: Record<string, string>
stripeProductId?: string
stripePriceId?: string
active: boolean
}
type ConvexManualDoc = {
filename: string
path: string
manufacturer: string
category: string
size?: number
lastModified?: number
searchTerms?: string[]
commonNames?: string[]
thumbnailUrl?: string
manualUrl?: string
}
const LIST_MANUALS = makeFunctionReference<"query">("manuals:list")
const MANUALS_DASHBOARD = makeFunctionReference<"query">("manuals:dashboard")
let cachedServerConvexAdminClient: ConvexHttpClient | null | undefined
let cachedServerConvexPublicClient: ConvexHttpClient | null | undefined
function getServerConvexClient(useAdminAuth: boolean) {
const cacheKey = useAdminAuth ? "admin" : "public"
const cachedClient = useAdminAuth
? cachedServerConvexAdminClient
: cachedServerConvexPublicClient
if (cachedClient !== undefined) {
return cachedClient
}
const convexUrl = process.env.CONVEX_URL || process.env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
if (useAdminAuth) {
cachedServerConvexAdminClient = null
return cachedServerConvexAdminClient
}
cachedServerConvexPublicClient = null
return cachedServerConvexPublicClient
}
const client = new ConvexHttpClient(convexUrl)
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY
if (useAdminAuth && adminKey) {
;(
client as ConvexHttpClient & {
setAdminAuth?: (token: string) => void
}
).setAdminAuth?.(adminKey)
}
if (cacheKey === "admin") {
cachedServerConvexAdminClient = client
return cachedServerConvexAdminClient
}
cachedServerConvexPublicClient = client
return cachedServerConvexPublicClient
}
async function queryManualsWithAuthFallback<TData>(
label: string,
queryRef: ReturnType<typeof makeFunctionReference<"query">>,
fallback: TData,
args: Record<string, unknown> = {}
): Promise<TData> {
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY
const adminClient = getServerConvexClient(true)
if (adminClient) {
try {
return (await adminClient.query(queryRef, args)) as TData
} catch (error) {
console.error(`[convex-service] ${label} admin query failed`, error)
if (!adminKey) {
return fallback
}
}
}
const publicClient = getServerConvexClient(false)
if (!publicClient) {
return fallback
}
return await safeFetchQuery(
`${label}.public`,
publicClient.query(queryRef, args),
fallback
)
}
type ConvexOrderItem = {
productId?: string | null
productName: string
price: number
quantity: number
priceId: string
image?: string
}
export type ConvexAdminOrder = {
id: string
customerEmail: string
customerName?: string
totalAmount: number
currency: string
status: "pending" | "paid" | "fulfilled" | "cancelled" | "refunded"
stripeSessionId: string | null
paymentIntentId: string | null
createdAt: string
updatedAt: string
shippingAddress?: {
name?: string
address?: string
city?: string
state?: string
zipCode?: string
country?: string
}
items: ConvexOrderItem[]
}
async function safeFetchQuery<TData>(
label: string,
query: Promise<TData>,
fallback: TData
): Promise<TData> {
try {
return await query
} catch (error) {
console.error(`[convex-service] ${label} failed`, error)
return fallback
}
}
function mapConvexProduct(product: ConvexProductDoc): Product {
return {
id: product._id,
stripeId: product.stripeProductId ?? product._id,
name: product.name,
description: product.description ?? null,
price: product.price,
currency: product.currency,
images: product.images ?? [],
metadata: product.metadata,
priceId: product.stripePriceId,
}
}
function mapConvexManual(manual: ConvexManualDoc): Manual {
return {
filename: manual.filename,
path: manual.path,
manufacturer: manual.manufacturer,
category: manual.category,
size: manual.size,
lastModified: manual.lastModified
? new Date(manual.lastModified)
: undefined,
searchTerms: manual.searchTerms,
commonNames: manual.commonNames,
thumbnailUrl: manual.thumbnailUrl,
}
}
export async function listConvexProducts(): Promise<Product[]> {
if (!hasConvexUrl()) {
return []
}
const products = await safeFetchQuery(
"products.listActive",
fetchQuery(api.products.listActive, {}),
[] as ConvexProductDoc[]
)
return (products as ConvexProductDoc[]).map(mapConvexProduct)
}
export async function listConvexAdminProducts(
search?: string
): Promise<Product[]> {
if (!hasConvexUrl()) {
return []
}
const products = await safeFetchQuery(
"products.listAdmin",
fetchQuery(api.products.listAdmin, { search }),
[] as ConvexProductDoc[]
)
return (products as ConvexProductDoc[]).map(mapConvexProduct)
}
export async function getConvexProduct(id: string): Promise<Product | null> {
if (!hasConvexUrl()) {
return null
}
const products = await safeFetchQuery(
"products.listAdmin",
fetchQuery(api.products.listAdmin, {}),
[] as ConvexProductDoc[]
)
const match = (products as ConvexProductDoc[]).find((product) => {
return product._id === id || product.stripeProductId === id
})
return match ? mapConvexProduct(match) : null
}
export async function listConvexManuals(domain?: string): Promise<Manual[]> {
if (!hasConvexUrl()) {
return []
}
const tenantDomain = resolveManualsTenantDomain({
requestHost: domain,
envTenantDomain: process.env.MANUALS_TENANT_DOMAIN,
envSiteDomain: process.env.NEXT_PUBLIC_SITE_DOMAIN,
})
if (!tenantDomain) {
return []
}
const manuals = await queryManualsWithAuthFallback(
"manuals.list",
LIST_MANUALS,
[] as ConvexManualDoc[],
{ domain: tenantDomain }
)
return (manuals as ConvexManualDoc[]).map(mapConvexManual)
}
export async function getConvexManualDashboard() {
if (!hasConvexUrl()) {
return null
}
return await queryManualsWithAuthFallback(
"manuals.dashboard",
MANUALS_DASHBOARD,
null
)
}
export async function getConvexOrderMetrics() {
if (!hasConvexUrl()) {
return null
}
return await safeFetchQuery(
"orders.getMetrics",
fetchQuery(api.orders.getMetrics, {}),
null
)
}
export async function listConvexOrders(options?: {
status?: ConvexAdminOrder["status"]
search?: string
}): Promise<ConvexAdminOrder[]> {
if (!hasConvexUrl()) {
return []
}
return await safeFetchQuery(
"orders.listAdmin",
fetchQuery(api.orders.listAdmin, {
status: options?.status,
search: options?.search,
}),
[] as ConvexAdminOrder[]
)
}