@supabase/stripe-sync-engine
Version:
Stripe Sync Engine to sync Stripe data based on webhooks to Postgres
234 lines (227 loc) • 11.8 kB
TypeScript
import Stripe from 'stripe';
import pg, { QueryResult } from 'pg';
import pino from 'pino';
interface EntitySchema {
readonly properties: string[];
}
type PostgresConfig = {
databaseUrl: string;
schema: string;
maxConnections?: number;
};
declare class PostgresClient {
private config;
pool: pg.Pool;
constructor(config: PostgresConfig);
delete(table: string, id: string): Promise<boolean>;
query(text: string, params?: string[]): Promise<QueryResult>;
upsertMany<T extends {
[Key: string]: any;
}>(entries: T[], table: string, tableSchema: EntitySchema): Promise<T[]>;
upsertManyWithTimestampProtection<T extends {
[Key: string]: any;
}>(entries: T[], table: string, tableSchema: EntitySchema, syncTimestamp?: string): Promise<T[]>;
findMissingEntries(table: string, ids: string[]): Promise<string[]>;
/**
* Returns an (yesql formatted) upsert function based on the key/vals of an object.
* eg,
* insert into customers ("id", "name")
* values (:id, :name)
* on conflict (id)
* do update set (
* "id" = :id,
* "name" = :name
* )
*/
private constructUpsertSql;
/**
* Returns an (yesql formatted) upsert function with timestamp protection.
*
* The WHERE clause in ON CONFLICT DO UPDATE only applies to the conflicting row
* (the row being updated), not to all rows in the table. PostgreSQL ensures that
* the condition is evaluated only for the specific row that conflicts with the INSERT.
*
*
* eg:
* INSERT INTO "stripe"."charges" (
* "id", "amount", "created", "last_synced_at"
* )
* VALUES (
* :id, :amount, :created, :last_synced_at
* )
* ON CONFLICT (id) DO UPDATE SET
* "amount" = EXCLUDED."amount",
* "created" = EXCLUDED."created",
* last_synced_at = :last_synced_at
* WHERE "charges"."last_synced_at" IS NULL
* OR "charges"."last_synced_at" < :last_synced_at;
*/
private constructUpsertWithTimestampProtectionSql;
/**
* For array object field like invoice.custom_fields
* ex: [{"name":"Project name","value":"Test Project"}]
*
* we need to stringify it first cos passing array object directly will end up with
* {
* invalid input syntax for type json
* detail: 'Expected ":", but found "}".',
* where: 'JSON data, line 1: ...\\":\\"Project name\\",\\"value\\":\\"Test Project\\"}"}',
* }
*/
private cleanseArrayField;
}
type RevalidateEntity = 'charge' | 'credit_note' | 'customer' | 'dispute' | 'invoice' | 'payment_intent' | 'payment_method' | 'plan' | 'price' | 'product' | 'refund' | 'review' | 'radar.early_fraud_warning' | 'setup_intent' | 'subscription' | 'subscription_schedule' | 'tax_id';
type StripeSyncConfig = {
/** Postgres database URL including authentication */
databaseUrl: string;
/** Database schema name. */
schema?: string;
/** Stripe secret key used to authenticate requests to the Stripe API. Defaults to empty string */
stripeSecretKey: string;
/** Webhook secret from Stripe to verify the signature of webhook events. */
stripeWebhookSecret: string;
/** Stripe API version for the webhooks, defaults to 2020-08-27 */
stripeApiVersion?: string;
/**
* Stripe limits related lists like invoice items in an invoice to 10 by default.
* By enabling this, sync-engine automatically fetches the remaining elements before saving
* */
autoExpandLists?: boolean;
/**
* If true, the sync engine will backfill related entities, i.e. when a invoice webhook comes in, it ensures that the customer is present and synced.
* This ensures foreign key integrity, but comes at the cost of additional queries to the database (and added latency for Stripe calls if the entity is actually missing).
*/
backfillRelatedEntities?: boolean;
/**
* If true, the webhook data is not used and instead the webhook is just a trigger to fetch the entity from Stripe again. This ensures that a race condition with failed webhooks can never accidentally overwrite the data with an older state.
*
* Default: false
*/
revalidateObjectsViaStripeApi?: Array<RevalidateEntity>;
maxPostgresConnections?: number;
logger?: pino.Logger;
};
type SyncObject = 'all' | 'customer' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions';
interface Sync {
synced: number;
}
interface SyncBackfill {
products?: Sync;
prices?: Sync;
plans?: Sync;
customers?: Sync;
subscriptions?: Sync;
subscriptionSchedules?: Sync;
invoices?: Sync;
setupIntents?: Sync;
paymentIntents?: Sync;
paymentMethods?: Sync;
disputes?: Sync;
charges?: Sync;
taxIds?: Sync;
creditNotes?: Sync;
earlyFraudWarnings?: Sync;
refunds?: Sync;
checkoutSessions?: Sync;
}
interface SyncBackfillParams {
created?: {
/**
* Minimum value to filter by (exclusive)
*/
gt?: number;
/**
* Minimum value to filter by (inclusive)
*/
gte?: number;
/**
* Maximum value to filter by (exclusive)
*/
lt?: number;
/**
* Maximum value to filter by (inclusive)
*/
lte?: number;
};
object?: SyncObject;
backfillRelatedEntities?: boolean;
}
declare class StripeSync {
private config;
stripe: Stripe;
postgresClient: PostgresClient;
constructor(config: StripeSyncConfig);
processWebhook(payload: Buffer | string, signature: string | undefined): Promise<void>;
processEvent(event: Stripe.Event): Promise<void>;
private getSyncTimestamp;
private shouldRefetchEntity;
private fetchOrUseWebhookData;
syncSingleEntity(stripeId: string): Promise<Stripe.Charge[] | (Stripe.DeletedCustomer | Stripe.Customer)[] | Stripe.Checkout.Session[] | Stripe.Subscription[] | Stripe.TaxId[] | Stripe.Invoice[] | Stripe.Product[] | Stripe.Price[] | Stripe.SetupIntent[] | Stripe.PaymentMethod[] | Stripe.Dispute[] | Stripe.PaymentIntent[] | Stripe.CreditNote[] | Stripe.Radar.EarlyFraudWarning[] | Stripe.Refund[] | Stripe.Review[] | undefined>;
syncBackfill(params?: SyncBackfillParams): Promise<SyncBackfill>;
syncProducts(syncParams?: SyncBackfillParams): Promise<Sync>;
syncPrices(syncParams?: SyncBackfillParams): Promise<Sync>;
syncPlans(syncParams?: SyncBackfillParams): Promise<Sync>;
syncCustomers(syncParams?: SyncBackfillParams): Promise<Sync>;
syncSubscriptions(syncParams?: SyncBackfillParams): Promise<Sync>;
syncSubscriptionSchedules(syncParams?: SyncBackfillParams): Promise<Sync>;
syncInvoices(syncParams?: SyncBackfillParams): Promise<Sync>;
syncCharges(syncParams?: SyncBackfillParams): Promise<Sync>;
syncSetupIntents(syncParams?: SyncBackfillParams): Promise<Sync>;
syncPaymentIntents(syncParams?: SyncBackfillParams): Promise<Sync>;
syncTaxIds(syncParams?: SyncBackfillParams): Promise<Sync>;
syncPaymentMethods(syncParams?: SyncBackfillParams): Promise<Sync>;
syncDisputes(syncParams?: SyncBackfillParams): Promise<Sync>;
syncEarlyFraudWarnings(syncParams?: SyncBackfillParams): Promise<Sync>;
syncRefunds(syncParams?: SyncBackfillParams): Promise<Sync>;
syncCreditNotes(syncParams?: SyncBackfillParams): Promise<Sync>;
syncCheckoutSessions(syncParams?: SyncBackfillParams): Promise<Sync>;
private fetchAndUpsert;
private upsertCharges;
private backfillCharges;
private backfillPaymentIntents;
private upsertCreditNotes;
upsertCheckoutSessions(checkoutSessions: Stripe.Checkout.Session[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Checkout.Session[]>;
upsertEarlyFraudWarning(earlyFraudWarnings: Stripe.Radar.EarlyFraudWarning[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Radar.EarlyFraudWarning[]>;
upsertRefunds(refunds: Stripe.Refund[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Refund[]>;
upsertReviews(reviews: Stripe.Review[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Review[]>;
upsertCustomers(customers: (Stripe.Customer | Stripe.DeletedCustomer)[], syncTimestamp?: string): Promise<(Stripe.Customer | Stripe.DeletedCustomer)[]>;
backfillCustomers(customerIds: string[]): Promise<void>;
upsertDisputes(disputes: Stripe.Dispute[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Dispute[]>;
upsertInvoices(invoices: Stripe.Invoice[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Invoice[]>;
backfillInvoices: (invoiceIds: string[]) => Promise<void>;
backfillPrices: (priceIds: string[]) => Promise<void>;
upsertPlans(plans: Stripe.Plan[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Plan[]>;
deletePlan(id: string): Promise<boolean>;
upsertPrices(prices: Stripe.Price[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Price[]>;
deletePrice(id: string): Promise<boolean>;
upsertProducts(products: Stripe.Product[], syncTimestamp?: string): Promise<Stripe.Product[]>;
deleteProduct(id: string): Promise<boolean>;
backfillProducts(productIds: string[]): Promise<void>;
upsertPaymentIntents(paymentIntents: Stripe.PaymentIntent[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.PaymentIntent[]>;
upsertPaymentMethods(paymentMethods: Stripe.PaymentMethod[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.PaymentMethod[]>;
upsertSetupIntents(setupIntents: Stripe.SetupIntent[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.SetupIntent[]>;
upsertTaxIds(taxIds: Stripe.TaxId[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.TaxId[]>;
deleteTaxId(id: string): Promise<boolean>;
upsertSubscriptionItems(subscriptionItems: Stripe.SubscriptionItem[], syncTimestamp?: string): Promise<void>;
fillCheckoutSessionsLineItems(checkoutSessionIds: string[], syncTimestamp?: string): Promise<void>;
upsertCheckoutSessionLineItems(lineItems: Stripe.LineItem[], checkoutSessionId: string, syncTimestamp?: string): Promise<void>;
markDeletedSubscriptionItems(subscriptionId: string, currentSubItemIds: string[]): Promise<{
rowCount: number;
}>;
upsertSubscriptionSchedules(subscriptionSchedules: Stripe.SubscriptionSchedule[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.SubscriptionSchedule[]>;
upsertSubscriptions(subscriptions: Stripe.Subscription[], backfillRelatedEntities?: boolean, syncTimestamp?: string): Promise<Stripe.Subscription[]>;
backfillSubscriptions(subscriptionIds: string[]): Promise<void>;
backfillSubscriptionSchedules: (subscriptionIds: string[]) => Promise<void>;
/**
* Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
*/
private expandEntity;
private fetchMissingEntities;
}
type MigrationConfig = {
schema: string;
databaseUrl: string;
logger?: pino.Logger;
};
declare function runMigrations(config: MigrationConfig): Promise<void>;
export { PostgresClient, type RevalidateEntity, StripeSync, type StripeSyncConfig, type Sync, type SyncBackfill, type SyncBackfillParams, type SyncObject, runMigrations };