UNPKG

@supabase/stripe-sync-engine

Version:

Stripe Sync Engine to sync Stripe data based on webhooks to Postgres

234 lines (227 loc) 11.8 kB
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 };