stripe-experiment-sync 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,68 +1,6 @@
1
- // package.json
2
- var package_default = {
3
- name: "stripe-experiment-sync",
4
- version: "1.0.1",
5
- private: false,
6
- description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
- type: "module",
8
- main: "./dist/index.cjs",
9
- exports: {
10
- import: {
11
- types: "./dist/index.d.ts",
12
- import: "./dist/index.js"
13
- },
14
- require: {
15
- types: "./dist/index.d.cts",
16
- require: "./dist/index.cjs"
17
- }
18
- },
19
- scripts: {
20
- clean: "rimraf dist",
21
- prebuild: "npm run clean",
22
- build: "tsup src/index.ts --format esm,cjs --dts --shims && cp -r src/database/migrations dist/migrations",
23
- lint: "eslint src --ext .ts",
24
- test: "vitest"
25
- },
26
- files: [
27
- "dist"
28
- ],
29
- dependencies: {
30
- pg: "^8.16.3",
31
- "pg-node-migrations": "0.0.8",
32
- ws: "^8.18.0",
33
- yesql: "^7.0.0"
34
- },
35
- peerDependencies: {
36
- stripe: "> 11"
37
- },
38
- devDependencies: {
39
- "@types/node": "^24.10.1",
40
- "@types/pg": "^8.15.5",
41
- "@types/ws": "^8.5.13",
42
- "@types/yesql": "^4.1.4",
43
- "@vitest/ui": "^4.0.9",
44
- vitest: "^3.2.4"
45
- },
46
- repository: {
47
- type: "git",
48
- url: "https://github.com/tx-stripe/stripe-sync-engine.git"
49
- },
50
- homepage: "https://github.com/tx-stripe/stripe-sync-engine#readme",
51
- bugs: {
52
- url: "https://github.com/tx-stripe/stripe-sync-engine/issues"
53
- },
54
- keywords: [
55
- "stripe",
56
- "postgres",
57
- "sync",
58
- "webhooks",
59
- "supabase",
60
- "billing",
61
- "database",
62
- "typescript"
63
- ],
64
- author: "Supabase <https://supabase.com/>"
65
- };
1
+ import {
2
+ package_default
3
+ } from "./chunk-SNGEJHKN.js";
66
4
 
67
5
  // src/stripeSync.ts
68
6
  import Stripe2 from "stripe";
@@ -398,12 +336,20 @@ var PostgresClient = class {
398
336
  */
399
337
  async cancelStaleRuns(accountId) {
400
338
  await this.query(
401
- `UPDATE "${this.config.schema}"."_sync_run" r
339
+ `UPDATE "${this.config.schema}"."_sync_obj_run" o
402
340
  SET status = 'error',
403
341
  error_message = 'Auto-cancelled: stale (no update in 5 min)',
404
342
  completed_at = now()
343
+ WHERE o."_account_id" = $1
344
+ AND o.status = 'running'
345
+ AND o.updated_at < now() - interval '5 minutes'`,
346
+ [accountId]
347
+ );
348
+ await this.query(
349
+ `UPDATE "${this.config.schema}"."_sync_run" r
350
+ SET closed_at = now()
405
351
  WHERE r."_account_id" = $1
406
- AND r.status = 'running'
352
+ AND r.closed_at IS NULL
407
353
  AND EXISTS (
408
354
  SELECT 1 FROM "${this.config.schema}"."_sync_obj_run" o
409
355
  WHERE o."_account_id" = r."_account_id"
@@ -413,7 +359,7 @@ var PostgresClient = class {
413
359
  SELECT 1 FROM "${this.config.schema}"."_sync_obj_run" o
414
360
  WHERE o."_account_id" = r."_account_id"
415
361
  AND o.run_started_at = r.started_at
416
- AND o.updated_at >= now() - interval '5 minutes'
362
+ AND o.status IN ('pending', 'running')
417
363
  )`,
418
364
  [accountId]
419
365
  );
@@ -429,7 +375,7 @@ var PostgresClient = class {
429
375
  await this.cancelStaleRuns(accountId);
430
376
  const existing = await this.query(
431
377
  `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_run"
432
- WHERE "_account_id" = $1 AND status = 'running'`,
378
+ WHERE "_account_id" = $1 AND closed_at IS NULL`,
433
379
  [accountId]
434
380
  );
435
381
  if (existing.rows.length > 0) {
@@ -458,7 +404,7 @@ var PostgresClient = class {
458
404
  async getActiveSyncRun(accountId) {
459
405
  const result = await this.query(
460
406
  `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_run"
461
- WHERE "_account_id" = $1 AND status = 'running'`,
407
+ WHERE "_account_id" = $1 AND closed_at IS NULL`,
462
408
  [accountId]
463
409
  );
464
410
  if (result.rows.length === 0) return null;
@@ -466,11 +412,12 @@ var PostgresClient = class {
466
412
  return { accountId: row._account_id, runStartedAt: row.started_at };
467
413
  }
468
414
  /**
469
- * Get full sync run details.
415
+ * Get sync run config (for concurrency control).
416
+ * Status is derived from sync_dashboard view.
470
417
  */
471
418
  async getSyncRun(accountId, runStartedAt) {
472
419
  const result = await this.query(
473
- `SELECT "_account_id", started_at, status, max_concurrent
420
+ `SELECT "_account_id", started_at, max_concurrent, closed_at
474
421
  FROM "${this.config.schema}"."_sync_run"
475
422
  WHERE "_account_id" = $1 AND started_at = $2`,
476
423
  [accountId, runStartedAt]
@@ -480,32 +427,22 @@ var PostgresClient = class {
480
427
  return {
481
428
  accountId: row._account_id,
482
429
  runStartedAt: row.started_at,
483
- status: row.status,
484
- maxConcurrent: row.max_concurrent
430
+ maxConcurrent: row.max_concurrent,
431
+ closedAt: row.closed_at
485
432
  };
486
433
  }
487
434
  /**
488
- * Mark a sync run as complete.
435
+ * Close a sync run (mark as done).
436
+ * Status (complete/error) is derived from object run states.
489
437
  */
490
- async completeSyncRun(accountId, runStartedAt) {
438
+ async closeSyncRun(accountId, runStartedAt) {
491
439
  await this.query(
492
440
  `UPDATE "${this.config.schema}"."_sync_run"
493
- SET status = 'complete', completed_at = now()
494
- WHERE "_account_id" = $1 AND started_at = $2`,
441
+ SET closed_at = now()
442
+ WHERE "_account_id" = $1 AND started_at = $2 AND closed_at IS NULL`,
495
443
  [accountId, runStartedAt]
496
444
  );
497
445
  }
498
- /**
499
- * Mark a sync run as failed.
500
- */
501
- async failSyncRun(accountId, runStartedAt, errorMessage) {
502
- await this.query(
503
- `UPDATE "${this.config.schema}"."_sync_run"
504
- SET status = 'error', error_message = $3, completed_at = now()
505
- WHERE "_account_id" = $1 AND started_at = $2`,
506
- [accountId, runStartedAt, errorMessage]
507
- );
508
- }
509
446
  /**
510
447
  * Create object run entries for a sync run.
511
448
  * All objects start as 'pending'.
@@ -629,6 +566,7 @@ var PostgresClient = class {
629
566
  }
630
567
  /**
631
568
  * Mark an object sync as complete.
569
+ * Auto-closes the run when all objects are done.
632
570
  */
633
571
  async completeObjectSync(accountId, runStartedAt, object) {
634
572
  await this.query(
@@ -637,9 +575,14 @@ var PostgresClient = class {
637
575
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
638
576
  [accountId, runStartedAt, object]
639
577
  );
578
+ const allDone = await this.areAllObjectsComplete(accountId, runStartedAt);
579
+ if (allDone) {
580
+ await this.closeSyncRun(accountId, runStartedAt);
581
+ }
640
582
  }
641
583
  /**
642
584
  * Mark an object sync as failed.
585
+ * Auto-closes the run when all objects are done.
643
586
  */
644
587
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
645
588
  await this.query(
@@ -648,6 +591,21 @@ var PostgresClient = class {
648
591
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
649
592
  [accountId, runStartedAt, object, errorMessage]
650
593
  );
594
+ const allDone = await this.areAllObjectsComplete(accountId, runStartedAt);
595
+ if (allDone) {
596
+ await this.closeSyncRun(accountId, runStartedAt);
597
+ }
598
+ }
599
+ /**
600
+ * Check if any object in a run has errored.
601
+ */
602
+ async hasAnyObjectErrors(accountId, runStartedAt) {
603
+ const result = await this.query(
604
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_run"
605
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND status = 'error'`,
606
+ [accountId, runStartedAt]
607
+ );
608
+ return parseInt(result.rows[0].count) > 0;
651
609
  }
652
610
  /**
653
611
  * Count running objects in a run.
@@ -689,6 +647,13 @@ var PostgresClient = class {
689
647
  );
690
648
  return parseInt(result.rows[0].count) === 0;
691
649
  }
650
+ /**
651
+ * Closes the database connection pool and cleans up resources.
652
+ * Call this when you're done using the PostgresClient instance.
653
+ */
654
+ async close() {
655
+ await this.pool.end();
656
+ }
692
657
  };
693
658
 
694
659
  // src/schemas/managed_webhook.ts
@@ -1243,8 +1208,8 @@ var StripeSync = class {
1243
1208
  // Depends on invoice
1244
1209
  listFn: (p) => this.stripe.creditNotes.list(p),
1245
1210
  upsertFn: (items, id, bf) => this.upsertCreditNotes(items, id, bf),
1246
- supportsCreatedFilter: false
1247
- // credit_notes don't support created filter
1211
+ supportsCreatedFilter: true
1212
+ // credit_notes support created filter
1248
1213
  },
1249
1214
  dispute: {
1250
1215
  order: 14,
@@ -1897,14 +1862,10 @@ var StripeSync = class {
1897
1862
  }
1898
1863
  }
1899
1864
  }
1900
- await this.postgresClient.completeSyncRun(accountId, runStartedAt);
1865
+ await this.postgresClient.closeSyncRun(accountId, runStartedAt);
1901
1866
  return results;
1902
1867
  } catch (error) {
1903
- await this.postgresClient.failSyncRun(
1904
- accountId,
1905
- runStartedAt,
1906
- error instanceof Error ? error.message : "Unknown error"
1907
- );
1868
+ await this.postgresClient.closeSyncRun(accountId, runStartedAt);
1908
1869
  throw error;
1909
1870
  }
1910
1871
  }
@@ -2435,12 +2396,13 @@ var StripeSync = class {
2435
2396
  await this.postgresClient.tryStartObjectSync(accountId, runStartedAt, resourceName);
2436
2397
  try {
2437
2398
  const result = await fn(cursor, runStartedAt);
2438
- await this.postgresClient.completeSyncRun(accountId, runStartedAt);
2399
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2439
2400
  return result;
2440
2401
  } catch (error) {
2441
- await this.postgresClient.failSyncRun(
2402
+ await this.postgresClient.failObjectSync(
2442
2403
  accountId,
2443
2404
  runStartedAt,
2405
+ resourceName,
2444
2406
  error instanceof Error ? error.message : "Unknown error"
2445
2407
  );
2446
2408
  throw error;
@@ -3188,6 +3150,13 @@ var StripeSync = class {
3188
3150
  }
3189
3151
  return entities;
3190
3152
  }
3153
+ /**
3154
+ * Closes the database connection pool and cleans up resources.
3155
+ * Call this when you're done using the StripeSync instance.
3156
+ */
3157
+ async close() {
3158
+ await this.postgresClient.pool.end();
3159
+ }
3191
3160
  };
3192
3161
  function chunkArray(array, chunkSize) {
3193
3162
  const result = [];
@@ -0,0 +1,72 @@
1
+ -- Fix generated columns: must drop and recreate with ::bigint cast
2
+ -- Money columns that can overflow PostgreSQL integer max (~2.1 billion)
3
+
4
+ -- checkout_session_line_items
5
+ ALTER TABLE "stripe"."checkout_session_line_items" DROP COLUMN "amount_discount";
6
+ ALTER TABLE "stripe"."checkout_session_line_items" ADD COLUMN "amount_discount" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_discount')::bigint) STORED;
7
+ ALTER TABLE "stripe"."checkout_session_line_items" DROP COLUMN "amount_subtotal";
8
+ ALTER TABLE "stripe"."checkout_session_line_items" ADD COLUMN "amount_subtotal" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_subtotal')::bigint) STORED;
9
+ ALTER TABLE "stripe"."checkout_session_line_items" DROP COLUMN "amount_tax";
10
+ ALTER TABLE "stripe"."checkout_session_line_items" ADD COLUMN "amount_tax" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_tax')::bigint) STORED;
11
+ ALTER TABLE "stripe"."checkout_session_line_items" DROP COLUMN "amount_total";
12
+ ALTER TABLE "stripe"."checkout_session_line_items" ADD COLUMN "amount_total" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_total')::bigint) STORED;
13
+
14
+ -- checkout_sessions
15
+ ALTER TABLE "stripe"."checkout_sessions" DROP COLUMN "amount_subtotal";
16
+ ALTER TABLE "stripe"."checkout_sessions" ADD COLUMN "amount_subtotal" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_subtotal')::bigint) STORED;
17
+ ALTER TABLE "stripe"."checkout_sessions" DROP COLUMN "amount_total";
18
+ ALTER TABLE "stripe"."checkout_sessions" ADD COLUMN "amount_total" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_total')::bigint) STORED;
19
+
20
+ -- credit_notes
21
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "amount";
22
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "amount" bigint GENERATED ALWAYS AS ((_raw_data->>'amount')::bigint) STORED;
23
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "amount_shipping";
24
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "amount_shipping" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_shipping')::bigint) STORED;
25
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "discount_amount";
26
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "discount_amount" bigint GENERATED ALWAYS AS ((_raw_data->>'discount_amount')::bigint) STORED;
27
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "out_of_band_amount";
28
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "out_of_band_amount" bigint GENERATED ALWAYS AS ((_raw_data->>'out_of_band_amount')::bigint) STORED;
29
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "subtotal";
30
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "subtotal" bigint GENERATED ALWAYS AS ((_raw_data->>'subtotal')::bigint) STORED;
31
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "subtotal_excluding_tax";
32
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "subtotal_excluding_tax" bigint GENERATED ALWAYS AS ((_raw_data->>'subtotal_excluding_tax')::bigint) STORED;
33
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "total";
34
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "total" bigint GENERATED ALWAYS AS ((_raw_data->>'total')::bigint) STORED;
35
+ ALTER TABLE "stripe"."credit_notes" DROP COLUMN "total_excluding_tax";
36
+ ALTER TABLE "stripe"."credit_notes" ADD COLUMN "total_excluding_tax" bigint GENERATED ALWAYS AS ((_raw_data->>'total_excluding_tax')::bigint) STORED;
37
+
38
+ -- customers
39
+ ALTER TABLE "stripe"."customers" DROP COLUMN "balance";
40
+ ALTER TABLE "stripe"."customers" ADD COLUMN "balance" bigint GENERATED ALWAYS AS ((_raw_data->>'balance')::bigint) STORED;
41
+
42
+ -- invoices
43
+ ALTER TABLE "stripe"."invoices" DROP COLUMN "ending_balance";
44
+ ALTER TABLE "stripe"."invoices" ADD COLUMN "ending_balance" bigint GENERATED ALWAYS AS ((_raw_data->>'ending_balance')::bigint) STORED;
45
+ ALTER TABLE "stripe"."invoices" DROP COLUMN "starting_balance";
46
+ ALTER TABLE "stripe"."invoices" ADD COLUMN "starting_balance" bigint GENERATED ALWAYS AS ((_raw_data->>'starting_balance')::bigint) STORED;
47
+ ALTER TABLE "stripe"."invoices" DROP COLUMN "subtotal";
48
+ ALTER TABLE "stripe"."invoices" ADD COLUMN "subtotal" bigint GENERATED ALWAYS AS ((_raw_data->>'subtotal')::bigint) STORED;
49
+ ALTER TABLE "stripe"."invoices" DROP COLUMN "tax";
50
+ ALTER TABLE "stripe"."invoices" ADD COLUMN "tax" bigint GENERATED ALWAYS AS ((_raw_data->>'tax')::bigint) STORED;
51
+ ALTER TABLE "stripe"."invoices" DROP COLUMN "post_payment_credit_notes_amount";
52
+ ALTER TABLE "stripe"."invoices" ADD COLUMN "post_payment_credit_notes_amount" bigint GENERATED ALWAYS AS ((_raw_data->>'post_payment_credit_notes_amount')::bigint) STORED;
53
+ ALTER TABLE "stripe"."invoices" DROP COLUMN "pre_payment_credit_notes_amount";
54
+ ALTER TABLE "stripe"."invoices" ADD COLUMN "pre_payment_credit_notes_amount" bigint GENERATED ALWAYS AS ((_raw_data->>'pre_payment_credit_notes_amount')::bigint) STORED;
55
+
56
+ -- payment_intents
57
+ ALTER TABLE "stripe"."payment_intents" DROP COLUMN "amount";
58
+ ALTER TABLE "stripe"."payment_intents" ADD COLUMN "amount" bigint GENERATED ALWAYS AS ((_raw_data->>'amount')::bigint) STORED;
59
+ ALTER TABLE "stripe"."payment_intents" DROP COLUMN "amount_capturable";
60
+ ALTER TABLE "stripe"."payment_intents" ADD COLUMN "amount_capturable" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_capturable')::bigint) STORED;
61
+ ALTER TABLE "stripe"."payment_intents" DROP COLUMN "amount_received";
62
+ ALTER TABLE "stripe"."payment_intents" ADD COLUMN "amount_received" bigint GENERATED ALWAYS AS ((_raw_data->>'amount_received')::bigint) STORED;
63
+ ALTER TABLE "stripe"."payment_intents" DROP COLUMN "application_fee_amount";
64
+ ALTER TABLE "stripe"."payment_intents" ADD COLUMN "application_fee_amount" bigint GENERATED ALWAYS AS ((_raw_data->>'application_fee_amount')::bigint) STORED;
65
+
66
+ -- prices
67
+ ALTER TABLE "stripe"."prices" DROP COLUMN "unit_amount";
68
+ ALTER TABLE "stripe"."prices" ADD COLUMN "unit_amount" bigint GENERATED ALWAYS AS ((_raw_data->>'unit_amount')::bigint) STORED;
69
+
70
+ -- refunds
71
+ ALTER TABLE "stripe"."refunds" DROP COLUMN "amount";
72
+ ALTER TABLE "stripe"."refunds" ADD COLUMN "amount" bigint GENERATED ALWAYS AS ((_raw_data->>'amount')::bigint) STORED;
@@ -0,0 +1,53 @@
1
+ -- Add closed_at column to _sync_run
2
+ -- closed_at IS NULL means the run is still active
3
+ -- Status is derived from object states when closed_at IS NOT NULL
4
+
5
+ -- Step 1: Drop dependent view first
6
+ DROP VIEW IF EXISTS "stripe"."sync_dashboard";
7
+
8
+ -- Step 2: Drop the old constraint, status column, and completed_at column
9
+ ALTER TABLE "stripe"."_sync_run" DROP CONSTRAINT IF EXISTS one_active_run_per_account;
10
+ ALTER TABLE "stripe"."_sync_run" DROP COLUMN IF EXISTS status;
11
+ ALTER TABLE "stripe"."_sync_run" DROP COLUMN IF EXISTS completed_at;
12
+
13
+ -- Step 3: Add closed_at column
14
+ ALTER TABLE "stripe"."_sync_run" ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
15
+
16
+ -- Step 4: Create exclusion constraint (only one active run per account)
17
+ ALTER TABLE "stripe"."_sync_run"
18
+ ADD CONSTRAINT one_active_run_per_account
19
+ EXCLUDE ("_account_id" WITH =) WHERE (closed_at IS NULL);
20
+
21
+ -- Step 5: Recreate sync_dashboard view (run-level only, one row per run)
22
+ -- Base table: _sync_run (parent sync operation)
23
+ -- Child table: _sync_obj_run (individual object syncs)
24
+ CREATE OR REPLACE VIEW "stripe"."sync_dashboard" AS
25
+ SELECT
26
+ run."_account_id" as account_id,
27
+ run.started_at,
28
+ run.closed_at,
29
+ run.max_concurrent,
30
+ run.triggered_by,
31
+ run.updated_at,
32
+ -- Derived status from object states
33
+ CASE
34
+ WHEN run.closed_at IS NULL THEN 'running'
35
+ WHEN EXISTS (
36
+ SELECT 1 FROM "stripe"."_sync_obj_run" obj
37
+ WHERE obj."_account_id" = run."_account_id"
38
+ AND obj.run_started_at = run.started_at
39
+ AND obj.status = 'error'
40
+ ) THEN 'error'
41
+ ELSE 'complete'
42
+ END as status,
43
+ -- First error message from failed objects
44
+ (SELECT obj.error_message FROM "stripe"."_sync_obj_run" obj
45
+ WHERE obj."_account_id" = run."_account_id"
46
+ AND obj.run_started_at = run.started_at
47
+ AND obj.status = 'error'
48
+ ORDER BY obj.object LIMIT 1) as error_message,
49
+ -- Total processed count across all objects
50
+ COALESCE((SELECT SUM(obj.processed_count) FROM "stripe"."_sync_obj_run" obj
51
+ WHERE obj."_account_id" = run."_account_id"
52
+ AND obj.run_started_at = run.started_at), 0) as processed_count
53
+ FROM "stripe"."_sync_run" run;