stripe-experiment-sync 1.0.17 → 1.0.19

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/README.md CHANGED
@@ -202,6 +202,9 @@ Supported objects: `all`, `charge`, `checkout_sessions`, `credit_note`, `custome
202
202
 
203
203
  The sync engine tracks cursors per account and resource, enabling incremental syncing that resumes after interruptions.
204
204
 
205
+ For paged backfills, the engine keeps a separate per-run pagination cursor (`page_cursor`) while the
206
+ incremental cursor continues to track the highest `created` timestamp.
207
+
205
208
  > **Tip:** For large Stripe accounts (>10,000 objects), loop through date ranges day-by-day to avoid timeouts.
206
209
 
207
210
  ## Account Management
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-57SXDCMH.js";
3
+ } from "./chunk-CMGFQCD7.js";
4
4
 
5
5
  // src/supabase/supabase.ts
6
6
  import { SupabaseManagementAPI } from "supabase-management-js";
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "stripe-experiment-sync",
4
- version: "1.0.17",
4
+ version: "1.0.19",
5
5
  private: false,
6
6
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
7
  type: "module",
@@ -2,11 +2,11 @@ import {
2
2
  StripeSync,
3
3
  createStripeWebSocketClient,
4
4
  runMigrations
5
- } from "./chunk-TV67ZOCK.js";
5
+ } from "./chunk-XKBCLBFT.js";
6
6
  import {
7
7
  install,
8
8
  uninstall
9
- } from "./chunk-I7IFXSAU.js";
9
+ } from "./chunk-4P6TAP7L.js";
10
10
 
11
11
  // src/cli/config.ts
12
12
  import dotenv from "dotenv";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-57SXDCMH.js";
3
+ } from "./chunk-CMGFQCD7.js";
4
4
 
5
5
  // src/stripeSync.ts
6
6
  import Stripe3 from "stripe";
@@ -398,7 +398,8 @@ var PostgresClient = class {
398
398
  `UPDATE "${this.config.schema}"."_sync_obj_runs" o
399
399
  SET status = 'error',
400
400
  error_message = 'Auto-cancelled: stale (no update in 5 min)',
401
- completed_at = now()
401
+ completed_at = now(),
402
+ page_cursor = NULL
402
403
  WHERE o."_account_id" = $1
403
404
  AND o.status = 'running'
404
405
  AND o.updated_at < now() - interval '5 minutes'`,
@@ -505,15 +506,17 @@ var PostgresClient = class {
505
506
  /**
506
507
  * Create object run entries for a sync run.
507
508
  * All objects start as 'pending'.
509
+ *
510
+ * @param resourceNames - Database resource names (e.g. 'products', 'customers', NOT 'product', 'customer')
508
511
  */
509
- async createObjectRuns(accountId, runStartedAt, objects) {
510
- if (objects.length === 0) return;
511
- const values = objects.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
512
+ async createObjectRuns(accountId, runStartedAt, resourceNames) {
513
+ if (resourceNames.length === 0) return;
514
+ const values = resourceNames.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
512
515
  await this.query(
513
516
  `INSERT INTO "${this.config.schema}"."_sync_obj_runs" ("_account_id", run_started_at, object)
514
517
  VALUES ${values}
515
518
  ON CONFLICT ("_account_id", run_started_at, object) DO NOTHING`,
516
- [accountId, runStartedAt, ...objects]
519
+ [accountId, runStartedAt, ...resourceNames]
517
520
  );
518
521
  }
519
522
  /**
@@ -542,7 +545,7 @@ var PostgresClient = class {
542
545
  */
543
546
  async getObjectRun(accountId, runStartedAt, object) {
544
547
  const result = await this.query(
545
- `SELECT object, status, processed_count, cursor
548
+ `SELECT object, status, processed_count, cursor, page_cursor
546
549
  FROM "${this.config.schema}"."_sync_obj_runs"
547
550
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
548
551
  [accountId, runStartedAt, object]
@@ -553,7 +556,8 @@ var PostgresClient = class {
553
556
  object: row.object,
554
557
  status: row.status,
555
558
  processedCount: row.processed_count,
556
- cursor: row.cursor
559
+ cursor: row.cursor,
560
+ pageCursor: row.page_cursor
557
561
  };
558
562
  }
559
563
  /**
@@ -568,6 +572,23 @@ var PostgresClient = class {
568
572
  [accountId, runStartedAt, object, count]
569
573
  );
570
574
  }
575
+ /**
576
+ * Update the pagination page_cursor used for backfills using Stripe list calls.
577
+ */
578
+ async updateObjectPageCursor(accountId, runStartedAt, object, pageCursor) {
579
+ await this.query(
580
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
581
+ SET page_cursor = $4, updated_at = now()
582
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
583
+ [accountId, runStartedAt, object, pageCursor]
584
+ );
585
+ }
586
+ /**
587
+ * Clear the pagination page_cursor for an object sync.
588
+ */
589
+ async clearObjectPageCursor(accountId, runStartedAt, object) {
590
+ await this.updateObjectPageCursor(accountId, runStartedAt, object, null);
591
+ }
571
592
  /**
572
593
  * Update the cursor for an object sync.
573
594
  * Only updates if the new cursor is higher than the existing one (cursors should never decrease).
@@ -600,9 +621,10 @@ var PostgresClient = class {
600
621
  }
601
622
  /**
602
623
  * Get the highest cursor from previous syncs for an object type.
603
- * This considers completed, error, AND running runs to ensure recovery syncs
604
- * don't re-process data that was already synced before a crash.
605
- * A 'running' status with a cursor means the process was killed mid-sync.
624
+ * Uses only completed object runs.
625
+ * - During the initial backfill we page through history, but we also update the cursor as we go.
626
+ * If we crash mid-backfill and reuse that cursor, we can accidentally switch into incremental mode
627
+ * too early and only ever fetch the newest page (breaking the historical backfill).
606
628
  *
607
629
  * Handles two cursor formats:
608
630
  * - Numeric: compared as bigint for correct ordering
@@ -617,11 +639,31 @@ var PostgresClient = class {
617
639
  FROM "${this.config.schema}"."_sync_obj_runs" o
618
640
  WHERE o."_account_id" = $1
619
641
  AND o.object = $2
620
- AND o.cursor IS NOT NULL`,
642
+ AND o.cursor IS NOT NULL
643
+ AND o.status = 'complete'`,
621
644
  [accountId, object]
622
645
  );
623
646
  return result.rows[0]?.cursor ?? null;
624
647
  }
648
+ /**
649
+ * Get the highest cursor from previous syncs for an object type, excluding the current run.
650
+ */
651
+ async getLastCursorBeforeRun(accountId, object, runStartedAt) {
652
+ const result = await this.query(
653
+ `SELECT CASE
654
+ WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
655
+ ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
656
+ END as cursor
657
+ FROM "${this.config.schema}"."_sync_obj_runs" o
658
+ WHERE o."_account_id" = $1
659
+ AND o.object = $2
660
+ AND o.cursor IS NOT NULL
661
+ AND o.status = 'complete'
662
+ AND o.run_started_at < $3`,
663
+ [accountId, object, runStartedAt]
664
+ );
665
+ return result.rows[0]?.cursor ?? null;
666
+ }
625
667
  /**
626
668
  * Delete all sync runs and object runs for an account.
627
669
  * Useful for testing or resetting sync state.
@@ -642,7 +684,7 @@ var PostgresClient = class {
642
684
  async completeObjectSync(accountId, runStartedAt, object) {
643
685
  await this.query(
644
686
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
645
- SET status = 'complete', completed_at = now()
687
+ SET status = 'complete', completed_at = now(), page_cursor = NULL
646
688
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
647
689
  [accountId, runStartedAt, object]
648
690
  );
@@ -658,7 +700,7 @@ var PostgresClient = class {
658
700
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
659
701
  await this.query(
660
702
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
661
- SET status = 'error', error_message = $4, completed_at = now()
703
+ SET status = 'error', error_message = $4, completed_at = now(), page_cursor = NULL
662
704
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
663
705
  [accountId, runStartedAt, object, errorMessage]
664
706
  );
@@ -1959,57 +2001,74 @@ var StripeSync = class {
1959
2001
  * ```
1960
2002
  */
1961
2003
  async processNext(object, params) {
1962
- await this.getCurrentAccount();
1963
- const accountId = await this.getAccountId();
1964
- const resourceName = this.getResourceName(object);
1965
- let runStartedAt;
1966
- if (params?.runStartedAt) {
1967
- runStartedAt = params.runStartedAt;
1968
- } else {
1969
- const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
1970
- runStartedAt = runKey.runStartedAt;
1971
- }
1972
- await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1973
- const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
1974
- if (objRun?.status === "complete" || objRun?.status === "error") {
1975
- return {
1976
- processed: 0,
1977
- hasMore: false,
1978
- runStartedAt
1979
- };
1980
- }
1981
- if (objRun?.status === "pending") {
1982
- const started = await this.postgresClient.tryStartObjectSync(
1983
- accountId,
1984
- runStartedAt,
1985
- resourceName
1986
- );
1987
- if (!started) {
2004
+ try {
2005
+ await this.getCurrentAccount();
2006
+ const accountId = await this.getAccountId();
2007
+ const resourceName = this.getResourceName(object);
2008
+ let runStartedAt;
2009
+ if (params?.runStartedAt) {
2010
+ runStartedAt = params.runStartedAt;
2011
+ } else {
2012
+ const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
2013
+ runStartedAt = runKey.runStartedAt;
2014
+ }
2015
+ await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2016
+ const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
2017
+ if (objRun?.status === "complete" || objRun?.status === "error") {
1988
2018
  return {
1989
2019
  processed: 0,
1990
- hasMore: true,
2020
+ hasMore: false,
1991
2021
  runStartedAt
1992
2022
  };
1993
2023
  }
1994
- }
1995
- let cursor = null;
1996
- if (!params?.created) {
1997
- if (objRun?.cursor) {
1998
- cursor = objRun.cursor;
1999
- } else {
2000
- const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
2024
+ if (objRun?.status === "pending") {
2025
+ const started = await this.postgresClient.tryStartObjectSync(
2026
+ accountId,
2027
+ runStartedAt,
2028
+ resourceName
2029
+ );
2030
+ if (!started) {
2031
+ return {
2032
+ processed: 0,
2033
+ hasMore: true,
2034
+ runStartedAt
2035
+ };
2036
+ }
2037
+ }
2038
+ let cursor = null;
2039
+ if (!params?.created) {
2040
+ const lastCursor = await this.postgresClient.getLastCursorBeforeRun(
2041
+ accountId,
2042
+ resourceName,
2043
+ runStartedAt
2044
+ );
2001
2045
  cursor = lastCursor ?? null;
2002
2046
  }
2047
+ const result = await this.fetchOnePage(
2048
+ object,
2049
+ accountId,
2050
+ resourceName,
2051
+ runStartedAt,
2052
+ cursor,
2053
+ objRun?.pageCursor ?? null,
2054
+ params
2055
+ );
2056
+ return result;
2057
+ } catch (error) {
2058
+ throw this.appendMigrationHint(error);
2003
2059
  }
2004
- const result = await this.fetchOnePage(
2005
- object,
2006
- accountId,
2007
- resourceName,
2008
- runStartedAt,
2009
- cursor,
2010
- params
2011
- );
2012
- return result;
2060
+ }
2061
+ appendMigrationHint(error) {
2062
+ const hint = "Error occurred. Make sure you are up to date with DB migrations which can sometimes help with this. Details:";
2063
+ const withHint = (message) => message.includes(hint) ? message : `${hint}
2064
+ ${message}`;
2065
+ if (error instanceof Error) {
2066
+ const { stack } = error;
2067
+ error.message = withHint(error.message);
2068
+ if (stack) error.stack = stack;
2069
+ return error;
2070
+ }
2071
+ return new Error(withHint(String(error)));
2013
2072
  }
2014
2073
  /**
2015
2074
  * Get the database resource name for a SyncObject type
@@ -2041,7 +2100,7 @@ var StripeSync = class {
2041
2100
  * Uses resourceRegistry for DRY list/upsert operations.
2042
2101
  * Uses the observable sync system for tracking progress.
2043
2102
  */
2044
- async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, params) {
2103
+ async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
2045
2104
  const limit = 100;
2046
2105
  if (object === "payment_method" || object === "tax_id") {
2047
2106
  this.config.logger?.warn(`processNext for ${object} requires customer context`);
@@ -2069,7 +2128,16 @@ var StripeSync = class {
2069
2128
  listParams.created = created;
2070
2129
  }
2071
2130
  }
2131
+ if (pageCursor) {
2132
+ listParams.starting_after = pageCursor;
2133
+ }
2072
2134
  const response = await config.listFn(listParams);
2135
+ if (response.data.length === 0 && response.has_more) {
2136
+ const message = `Stripe returned has_more=true with empty page for ${resourceName}. Aborting to avoid infinite loop.`;
2137
+ this.config.logger?.warn(message);
2138
+ await this.postgresClient.failObjectSync(accountId, runStartedAt, resourceName, message);
2139
+ return { processed: 0, hasMore: false, runStartedAt };
2140
+ }
2073
2141
  if (response.data.length > 0) {
2074
2142
  this.config.logger?.info(`processNext: upserting ${response.data.length} ${resourceName}`);
2075
2143
  await config.upsertFn(response.data, accountId, params?.backfillRelatedEntities);
@@ -2090,6 +2158,15 @@ var StripeSync = class {
2090
2158
  String(maxCreated)
2091
2159
  );
2092
2160
  }
2161
+ const lastId = response.data[response.data.length - 1].id;
2162
+ if (response.has_more) {
2163
+ await this.postgresClient.updateObjectPageCursor(
2164
+ accountId,
2165
+ runStartedAt,
2166
+ resourceName,
2167
+ lastId
2168
+ );
2169
+ }
2093
2170
  }
2094
2171
  if (!response.has_more) {
2095
2172
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
@@ -2238,31 +2315,43 @@ var StripeSync = class {
2238
2315
  * This is used by workers and background processes that should cooperate.
2239
2316
  *
2240
2317
  * @param triggeredBy - What triggered this sync (for observability)
2318
+ * @param objectFilter - Optional specific object to sync (e.g. 'payment_intent'). If 'all' or undefined, syncs all objects.
2241
2319
  * @returns Run key and list of objects to sync
2242
2320
  */
2243
- async joinOrCreateSyncRun(triggeredBy = "worker") {
2321
+ async joinOrCreateSyncRun(triggeredBy = "worker", objectFilter) {
2244
2322
  await this.getCurrentAccount();
2245
2323
  const accountId = await this.getAccountId();
2246
2324
  const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
2325
+ const objects = objectFilter === "all" || objectFilter === void 0 ? this.getSupportedSyncObjects() : [objectFilter];
2247
2326
  if (!result) {
2248
2327
  const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
2249
2328
  if (!activeRun) {
2250
2329
  throw new Error("Failed to get or create sync run");
2251
2330
  }
2331
+ await this.postgresClient.createObjectRuns(
2332
+ activeRun.accountId,
2333
+ activeRun.runStartedAt,
2334
+ objects.map((obj) => this.getResourceName(obj))
2335
+ );
2252
2336
  return {
2253
2337
  runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
2254
- objects: this.getSupportedSyncObjects()
2338
+ objects
2255
2339
  };
2256
2340
  }
2257
2341
  const { accountId: runAccountId, runStartedAt } = result;
2342
+ await this.postgresClient.createObjectRuns(
2343
+ runAccountId,
2344
+ runStartedAt,
2345
+ objects.map((obj) => this.getResourceName(obj))
2346
+ );
2258
2347
  return {
2259
2348
  runKey: { accountId: runAccountId, runStartedAt },
2260
- objects: this.getSupportedSyncObjects()
2349
+ objects
2261
2350
  };
2262
2351
  }
2263
2352
  async processUntilDone(params) {
2264
2353
  const { object } = params ?? { object: "all" };
2265
- const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
2354
+ const { runKey } = await this.joinOrCreateSyncRun("processUntilDone", object);
2266
2355
  return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
2267
2356
  }
2268
2357
  /**