From 8a40db92d2a4631655158a767dcfba762271acbc Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Mon, 16 Jun 2025 22:32:23 -0400 Subject: [PATCH] add syncProducts function --- README.md | 13 +++++++ example/convex/_generated/api.d.ts | 57 ++++++++++++++++++++++++++++++ src/client/index.ts | 6 ++++ src/component/_generated/api.d.ts | 57 ++++++++++++++++++++++++++++++ src/component/lib.ts | 53 +++++++++++++++++++++++++-- 5 files changed, 184 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 67c9f3f..6eecc85 100644 --- a/README.md +++ b/README.md @@ -472,3 +472,16 @@ polar.registerRoutes(http, { ``` The webhook handler uses the `webhookSecret` from the Polar client configuration or the `POLAR_WEBHOOK_SECRET` environment variable. + +#### syncProducts + +Sync existing products from Polar (must be run inside an action): + +```ts +export const syncProducts = action({ + args: {}, + handler: async (ctx) => { + await polar.syncProducts(ctx); + }, +}); +``` diff --git a/example/convex/_generated/api.d.ts b/example/convex/_generated/api.d.ts index e6e261a..c542451 100644 --- a/example/convex/_generated/api.d.ts +++ b/example/convex/_generated/api.d.ts @@ -421,6 +421,12 @@ export declare const components: { status: string; }> >; + syncProducts: FunctionReference< + "action", + "internal", + { polarAccessToken: string; server: "sandbox" | "production" }, + any + >; updateProduct: FunctionReference< "mutation", "internal", @@ -471,6 +477,57 @@ export declare const components: { }, any >; + updateProducts: FunctionReference< + "mutation", + "internal", + { + polarAccessToken: string; + products: Array<{ + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + modifiedAt: string | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "month" | "year" | null; + }>; + }, + any + >; updateSubscription: FunctionReference< "mutation", "internal", diff --git a/src/client/index.ts b/src/client/index.ts index ebe4019..9cdc8fc 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -85,6 +85,12 @@ export class Polar< getCustomerByUserId(ctx: RunQueryCtx, userId: string) { return ctx.runQuery(this.component.lib.getCustomerByUserId, { userId }); } + async syncProducts(ctx: RunActionCtx) { + await ctx.runAction(this.component.lib.syncProducts, { + polarAccessToken: this.organizationToken, + server: this.server, + }); + } async createCheckoutSession( ctx: RunMutationCtx, { diff --git a/src/component/_generated/api.d.ts b/src/component/_generated/api.d.ts index 90e3365..835253c 100644 --- a/src/component/_generated/api.d.ts +++ b/src/component/_generated/api.d.ts @@ -407,6 +407,12 @@ export type Mounts = { status: string; }> >; + syncProducts: FunctionReference< + "action", + "public", + { polarAccessToken: string; server: "sandbox" | "production" }, + any + >; updateProduct: FunctionReference< "mutation", "public", @@ -457,6 +463,57 @@ export type Mounts = { }, any >; + updateProducts: FunctionReference< + "mutation", + "public", + { + polarAccessToken: string; + products: Array<{ + createdAt: string; + description: string | null; + id: string; + isArchived: boolean; + isRecurring: boolean; + medias: Array<{ + checksumEtag: string | null; + checksumSha256Base64: string | null; + checksumSha256Hex: string | null; + createdAt: string; + id: string; + isUploaded: boolean; + lastModifiedAt: string | null; + mimeType: string; + name: string; + organizationId: string; + path: string; + publicUrl: string; + service?: string; + size: number; + sizeReadable: string; + storageVersion: string | null; + version: string | null; + }>; + metadata?: Record; + modifiedAt: string | null; + name: string; + organizationId: string; + prices: Array<{ + amountType?: string; + createdAt: string; + id: string; + isArchived: boolean; + modifiedAt: string | null; + priceAmount?: number; + priceCurrency?: string; + productId: string; + recurringInterval?: "month" | "year" | null; + type?: string; + }>; + recurringInterval?: "month" | "year" | null; + }>; + }, + any + >; updateSubscription: FunctionReference< "mutation", "public", diff --git a/src/component/lib.ts b/src/component/lib.ts index 6aff1e0..3e6fa3a 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -1,8 +1,10 @@ +import { Polar as PolarSdk } from "@polar-sh/sdk"; import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; +import { action, mutation, query } from "./_generated/server"; import schema from "./schema"; import { asyncMap } from "convex-helpers"; -import { omitSystemFields } from "./util"; +import { api } from "./_generated/api"; +import { convertToDatabaseProduct, omitSystemFields } from "./util"; export const getCustomerByUserId = query({ args: { @@ -269,3 +271,50 @@ export const listCustomerSubscriptions = query({ return subscriptions.map(omitSystemFields); }, }); + +export const syncProducts = action({ + args: { + polarAccessToken: v.string(), + server: v.union(v.literal("sandbox"), v.literal("production")), + }, + handler: async (ctx, args) => { + const sdk = new PolarSdk({ + accessToken: args.polarAccessToken, + server: args.server, + }); + let page = 1; + let maxPage; + do { + const products = await sdk.products.list({ + page, + limit: 100, + }); + page = page + 1; + maxPage = products.result.pagination.maxPage; + await ctx.runMutation(api.lib.updateProducts, { + polarAccessToken: args.polarAccessToken, + products: products.result.items.map(convertToDatabaseProduct), + }); + } while (maxPage >= page); + }, +}); + +export const updateProducts = mutation({ + args: { + polarAccessToken: v.string(), + products: v.array(schema.tables.products.validator), + }, + handler: async (ctx, args) => { + await asyncMap(args.products, async (product) => { + const existingProduct = await ctx.db + .query("products") + .withIndex("id", (q) => q.eq("id", product.id)) + .unique(); + if (existingProduct) { + await ctx.db.patch(existingProduct._id, product); + return; + } + await ctx.db.insert("products", product); + }); + }, +});