add syncProducts function

This commit is contained in:
Shawn Erquhart
2025-06-16 22:32:23 -04:00
parent 3b633519a8
commit 8a40db92d2
5 changed files with 184 additions and 2 deletions

View File

@@ -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);
},
});
```

View File

@@ -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<string, any>;
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",

View File

@@ -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,
{

View File

@@ -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<string, any>;
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",

View File

@@ -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);
});
},
});