stripe-experiment-sync 1.0.18 → 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-XO57OJUE.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.18",
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-K4JQHI7Y.js";
5
+ } from "./chunk-XKBCLBFT.js";
6
6
  import {
7
7
  install,
8
8
  uninstall
9
- } from "./chunk-M5TTM27L.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-XO57OJUE.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'`,
@@ -544,7 +545,7 @@ var PostgresClient = class {
544
545
  */
545
546
  async getObjectRun(accountId, runStartedAt, object) {
546
547
  const result = await this.query(
547
- `SELECT object, status, processed_count, cursor
548
+ `SELECT object, status, processed_count, cursor, page_cursor
548
549
  FROM "${this.config.schema}"."_sync_obj_runs"
549
550
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
550
551
  [accountId, runStartedAt, object]
@@ -555,7 +556,8 @@ var PostgresClient = class {
555
556
  object: row.object,
556
557
  status: row.status,
557
558
  processedCount: row.processed_count,
558
- cursor: row.cursor
559
+ cursor: row.cursor,
560
+ pageCursor: row.page_cursor
559
561
  };
560
562
  }
561
563
  /**
@@ -570,6 +572,23 @@ var PostgresClient = class {
570
572
  [accountId, runStartedAt, object, count]
571
573
  );
572
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
+ }
573
592
  /**
574
593
  * Update the cursor for an object sync.
575
594
  * Only updates if the new cursor is higher than the existing one (cursors should never decrease).
@@ -602,9 +621,10 @@ var PostgresClient = class {
602
621
  }
603
622
  /**
604
623
  * Get the highest cursor from previous syncs for an object type.
605
- * This considers completed, error, AND running runs to ensure recovery syncs
606
- * don't re-process data that was already synced before a crash.
607
- * 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).
608
628
  *
609
629
  * Handles two cursor formats:
610
630
  * - Numeric: compared as bigint for correct ordering
@@ -619,11 +639,31 @@ var PostgresClient = class {
619
639
  FROM "${this.config.schema}"."_sync_obj_runs" o
620
640
  WHERE o."_account_id" = $1
621
641
  AND o.object = $2
622
- AND o.cursor IS NOT NULL`,
642
+ AND o.cursor IS NOT NULL
643
+ AND o.status = 'complete'`,
623
644
  [accountId, object]
624
645
  );
625
646
  return result.rows[0]?.cursor ?? null;
626
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
+ }
627
667
  /**
628
668
  * Delete all sync runs and object runs for an account.
629
669
  * Useful for testing or resetting sync state.
@@ -644,7 +684,7 @@ var PostgresClient = class {
644
684
  async completeObjectSync(accountId, runStartedAt, object) {
645
685
  await this.query(
646
686
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
647
- SET status = 'complete', completed_at = now()
687
+ SET status = 'complete', completed_at = now(), page_cursor = NULL
648
688
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
649
689
  [accountId, runStartedAt, object]
650
690
  );
@@ -660,7 +700,7 @@ var PostgresClient = class {
660
700
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
661
701
  await this.query(
662
702
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
663
- SET status = 'error', error_message = $4, completed_at = now()
703
+ SET status = 'error', error_message = $4, completed_at = now(), page_cursor = NULL
664
704
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
665
705
  [accountId, runStartedAt, object, errorMessage]
666
706
  );
@@ -1961,57 +2001,74 @@ var StripeSync = class {
1961
2001
  * ```
1962
2002
  */
1963
2003
  async processNext(object, params) {
1964
- await this.getCurrentAccount();
1965
- const accountId = await this.getAccountId();
1966
- const resourceName = this.getResourceName(object);
1967
- let runStartedAt;
1968
- if (params?.runStartedAt) {
1969
- runStartedAt = params.runStartedAt;
1970
- } else {
1971
- const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
1972
- runStartedAt = runKey.runStartedAt;
1973
- }
1974
- await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1975
- const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
1976
- if (objRun?.status === "complete" || objRun?.status === "error") {
1977
- return {
1978
- processed: 0,
1979
- hasMore: false,
1980
- runStartedAt
1981
- };
1982
- }
1983
- if (objRun?.status === "pending") {
1984
- const started = await this.postgresClient.tryStartObjectSync(
1985
- accountId,
1986
- runStartedAt,
1987
- resourceName
1988
- );
1989
- 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") {
1990
2018
  return {
1991
2019
  processed: 0,
1992
- hasMore: true,
2020
+ hasMore: false,
1993
2021
  runStartedAt
1994
2022
  };
1995
2023
  }
1996
- }
1997
- let cursor = null;
1998
- if (!params?.created) {
1999
- if (objRun?.cursor) {
2000
- cursor = objRun.cursor;
2001
- } else {
2002
- 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
+ );
2003
2045
  cursor = lastCursor ?? null;
2004
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);
2005
2059
  }
2006
- const result = await this.fetchOnePage(
2007
- object,
2008
- accountId,
2009
- resourceName,
2010
- runStartedAt,
2011
- cursor,
2012
- params
2013
- );
2014
- 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)));
2015
2072
  }
2016
2073
  /**
2017
2074
  * Get the database resource name for a SyncObject type
@@ -2043,7 +2100,7 @@ var StripeSync = class {
2043
2100
  * Uses resourceRegistry for DRY list/upsert operations.
2044
2101
  * Uses the observable sync system for tracking progress.
2045
2102
  */
2046
- async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, params) {
2103
+ async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
2047
2104
  const limit = 100;
2048
2105
  if (object === "payment_method" || object === "tax_id") {
2049
2106
  this.config.logger?.warn(`processNext for ${object} requires customer context`);
@@ -2071,7 +2128,16 @@ var StripeSync = class {
2071
2128
  listParams.created = created;
2072
2129
  }
2073
2130
  }
2131
+ if (pageCursor) {
2132
+ listParams.starting_after = pageCursor;
2133
+ }
2074
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
+ }
2075
2141
  if (response.data.length > 0) {
2076
2142
  this.config.logger?.info(`processNext: upserting ${response.data.length} ${resourceName}`);
2077
2143
  await config.upsertFn(response.data, accountId, params?.backfillRelatedEntities);
@@ -2092,6 +2158,15 @@ var StripeSync = class {
2092
2158
  String(maxCreated)
2093
2159
  );
2094
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
+ }
2095
2170
  }
2096
2171
  if (!response.has_more) {
2097
2172
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
@@ -33,7 +33,7 @@ var import_commander = require("commander");
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "stripe-experiment-sync",
36
- version: "1.0.18",
36
+ version: "1.0.19",
37
37
  private: false,
38
38
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
39
39
  type: "module",
@@ -580,7 +580,8 @@ var PostgresClient = class {
580
580
  `UPDATE "${this.config.schema}"."_sync_obj_runs" o
581
581
  SET status = 'error',
582
582
  error_message = 'Auto-cancelled: stale (no update in 5 min)',
583
- completed_at = now()
583
+ completed_at = now(),
584
+ page_cursor = NULL
584
585
  WHERE o."_account_id" = $1
585
586
  AND o.status = 'running'
586
587
  AND o.updated_at < now() - interval '5 minutes'`,
@@ -726,7 +727,7 @@ var PostgresClient = class {
726
727
  */
727
728
  async getObjectRun(accountId, runStartedAt, object) {
728
729
  const result = await this.query(
729
- `SELECT object, status, processed_count, cursor
730
+ `SELECT object, status, processed_count, cursor, page_cursor
730
731
  FROM "${this.config.schema}"."_sync_obj_runs"
731
732
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
732
733
  [accountId, runStartedAt, object]
@@ -737,7 +738,8 @@ var PostgresClient = class {
737
738
  object: row.object,
738
739
  status: row.status,
739
740
  processedCount: row.processed_count,
740
- cursor: row.cursor
741
+ cursor: row.cursor,
742
+ pageCursor: row.page_cursor
741
743
  };
742
744
  }
743
745
  /**
@@ -752,6 +754,23 @@ var PostgresClient = class {
752
754
  [accountId, runStartedAt, object, count]
753
755
  );
754
756
  }
757
+ /**
758
+ * Update the pagination page_cursor used for backfills using Stripe list calls.
759
+ */
760
+ async updateObjectPageCursor(accountId, runStartedAt, object, pageCursor) {
761
+ await this.query(
762
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
763
+ SET page_cursor = $4, updated_at = now()
764
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
765
+ [accountId, runStartedAt, object, pageCursor]
766
+ );
767
+ }
768
+ /**
769
+ * Clear the pagination page_cursor for an object sync.
770
+ */
771
+ async clearObjectPageCursor(accountId, runStartedAt, object) {
772
+ await this.updateObjectPageCursor(accountId, runStartedAt, object, null);
773
+ }
755
774
  /**
756
775
  * Update the cursor for an object sync.
757
776
  * Only updates if the new cursor is higher than the existing one (cursors should never decrease).
@@ -784,9 +803,10 @@ var PostgresClient = class {
784
803
  }
785
804
  /**
786
805
  * Get the highest cursor from previous syncs for an object type.
787
- * This considers completed, error, AND running runs to ensure recovery syncs
788
- * don't re-process data that was already synced before a crash.
789
- * A 'running' status with a cursor means the process was killed mid-sync.
806
+ * Uses only completed object runs.
807
+ * - During the initial backfill we page through history, but we also update the cursor as we go.
808
+ * If we crash mid-backfill and reuse that cursor, we can accidentally switch into incremental mode
809
+ * too early and only ever fetch the newest page (breaking the historical backfill).
790
810
  *
791
811
  * Handles two cursor formats:
792
812
  * - Numeric: compared as bigint for correct ordering
@@ -801,11 +821,31 @@ var PostgresClient = class {
801
821
  FROM "${this.config.schema}"."_sync_obj_runs" o
802
822
  WHERE o."_account_id" = $1
803
823
  AND o.object = $2
804
- AND o.cursor IS NOT NULL`,
824
+ AND o.cursor IS NOT NULL
825
+ AND o.status = 'complete'`,
805
826
  [accountId, object]
806
827
  );
807
828
  return result.rows[0]?.cursor ?? null;
808
829
  }
830
+ /**
831
+ * Get the highest cursor from previous syncs for an object type, excluding the current run.
832
+ */
833
+ async getLastCursorBeforeRun(accountId, object, runStartedAt) {
834
+ const result = await this.query(
835
+ `SELECT CASE
836
+ WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
837
+ ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
838
+ END as cursor
839
+ FROM "${this.config.schema}"."_sync_obj_runs" o
840
+ WHERE o."_account_id" = $1
841
+ AND o.object = $2
842
+ AND o.cursor IS NOT NULL
843
+ AND o.status = 'complete'
844
+ AND o.run_started_at < $3`,
845
+ [accountId, object, runStartedAt]
846
+ );
847
+ return result.rows[0]?.cursor ?? null;
848
+ }
809
849
  /**
810
850
  * Delete all sync runs and object runs for an account.
811
851
  * Useful for testing or resetting sync state.
@@ -826,7 +866,7 @@ var PostgresClient = class {
826
866
  async completeObjectSync(accountId, runStartedAt, object) {
827
867
  await this.query(
828
868
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
829
- SET status = 'complete', completed_at = now()
869
+ SET status = 'complete', completed_at = now(), page_cursor = NULL
830
870
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
831
871
  [accountId, runStartedAt, object]
832
872
  );
@@ -842,7 +882,7 @@ var PostgresClient = class {
842
882
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
843
883
  await this.query(
844
884
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
845
- SET status = 'error', error_message = $4, completed_at = now()
885
+ SET status = 'error', error_message = $4, completed_at = now(), page_cursor = NULL
846
886
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
847
887
  [accountId, runStartedAt, object, errorMessage]
848
888
  );
@@ -2143,57 +2183,74 @@ var StripeSync = class {
2143
2183
  * ```
2144
2184
  */
2145
2185
  async processNext(object, params) {
2146
- await this.getCurrentAccount();
2147
- const accountId = await this.getAccountId();
2148
- const resourceName = this.getResourceName(object);
2149
- let runStartedAt;
2150
- if (params?.runStartedAt) {
2151
- runStartedAt = params.runStartedAt;
2152
- } else {
2153
- const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
2154
- runStartedAt = runKey.runStartedAt;
2155
- }
2156
- await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2157
- const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
2158
- if (objRun?.status === "complete" || objRun?.status === "error") {
2159
- return {
2160
- processed: 0,
2161
- hasMore: false,
2162
- runStartedAt
2163
- };
2164
- }
2165
- if (objRun?.status === "pending") {
2166
- const started = await this.postgresClient.tryStartObjectSync(
2167
- accountId,
2168
- runStartedAt,
2169
- resourceName
2170
- );
2171
- if (!started) {
2186
+ try {
2187
+ await this.getCurrentAccount();
2188
+ const accountId = await this.getAccountId();
2189
+ const resourceName = this.getResourceName(object);
2190
+ let runStartedAt;
2191
+ if (params?.runStartedAt) {
2192
+ runStartedAt = params.runStartedAt;
2193
+ } else {
2194
+ const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
2195
+ runStartedAt = runKey.runStartedAt;
2196
+ }
2197
+ await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2198
+ const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
2199
+ if (objRun?.status === "complete" || objRun?.status === "error") {
2172
2200
  return {
2173
2201
  processed: 0,
2174
- hasMore: true,
2202
+ hasMore: false,
2175
2203
  runStartedAt
2176
2204
  };
2177
2205
  }
2178
- }
2179
- let cursor = null;
2180
- if (!params?.created) {
2181
- if (objRun?.cursor) {
2182
- cursor = objRun.cursor;
2183
- } else {
2184
- const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
2206
+ if (objRun?.status === "pending") {
2207
+ const started = await this.postgresClient.tryStartObjectSync(
2208
+ accountId,
2209
+ runStartedAt,
2210
+ resourceName
2211
+ );
2212
+ if (!started) {
2213
+ return {
2214
+ processed: 0,
2215
+ hasMore: true,
2216
+ runStartedAt
2217
+ };
2218
+ }
2219
+ }
2220
+ let cursor = null;
2221
+ if (!params?.created) {
2222
+ const lastCursor = await this.postgresClient.getLastCursorBeforeRun(
2223
+ accountId,
2224
+ resourceName,
2225
+ runStartedAt
2226
+ );
2185
2227
  cursor = lastCursor ?? null;
2186
2228
  }
2229
+ const result = await this.fetchOnePage(
2230
+ object,
2231
+ accountId,
2232
+ resourceName,
2233
+ runStartedAt,
2234
+ cursor,
2235
+ objRun?.pageCursor ?? null,
2236
+ params
2237
+ );
2238
+ return result;
2239
+ } catch (error) {
2240
+ throw this.appendMigrationHint(error);
2187
2241
  }
2188
- const result = await this.fetchOnePage(
2189
- object,
2190
- accountId,
2191
- resourceName,
2192
- runStartedAt,
2193
- cursor,
2194
- params
2195
- );
2196
- return result;
2242
+ }
2243
+ appendMigrationHint(error) {
2244
+ const hint = "Error occurred. Make sure you are up to date with DB migrations which can sometimes help with this. Details:";
2245
+ const withHint = (message) => message.includes(hint) ? message : `${hint}
2246
+ ${message}`;
2247
+ if (error instanceof Error) {
2248
+ const { stack } = error;
2249
+ error.message = withHint(error.message);
2250
+ if (stack) error.stack = stack;
2251
+ return error;
2252
+ }
2253
+ return new Error(withHint(String(error)));
2197
2254
  }
2198
2255
  /**
2199
2256
  * Get the database resource name for a SyncObject type
@@ -2225,7 +2282,7 @@ var StripeSync = class {
2225
2282
  * Uses resourceRegistry for DRY list/upsert operations.
2226
2283
  * Uses the observable sync system for tracking progress.
2227
2284
  */
2228
- async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, params) {
2285
+ async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
2229
2286
  const limit = 100;
2230
2287
  if (object === "payment_method" || object === "tax_id") {
2231
2288
  this.config.logger?.warn(`processNext for ${object} requires customer context`);
@@ -2253,7 +2310,16 @@ var StripeSync = class {
2253
2310
  listParams.created = created;
2254
2311
  }
2255
2312
  }
2313
+ if (pageCursor) {
2314
+ listParams.starting_after = pageCursor;
2315
+ }
2256
2316
  const response = await config.listFn(listParams);
2317
+ if (response.data.length === 0 && response.has_more) {
2318
+ const message = `Stripe returned has_more=true with empty page for ${resourceName}. Aborting to avoid infinite loop.`;
2319
+ this.config.logger?.warn(message);
2320
+ await this.postgresClient.failObjectSync(accountId, runStartedAt, resourceName, message);
2321
+ return { processed: 0, hasMore: false, runStartedAt };
2322
+ }
2257
2323
  if (response.data.length > 0) {
2258
2324
  this.config.logger?.info(`processNext: upserting ${response.data.length} ${resourceName}`);
2259
2325
  await config.upsertFn(response.data, accountId, params?.backfillRelatedEntities);
@@ -2274,6 +2340,15 @@ var StripeSync = class {
2274
2340
  String(maxCreated)
2275
2341
  );
2276
2342
  }
2343
+ const lastId = response.data[response.data.length - 1].id;
2344
+ if (response.has_more) {
2345
+ await this.postgresClient.updateObjectPageCursor(
2346
+ accountId,
2347
+ runStartedAt,
2348
+ resourceName,
2349
+ lastId
2350
+ );
2351
+ }
2277
2352
  }
2278
2353
  if (!response.has_more) {
2279
2354
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
package/dist/cli/index.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  migrateCommand,
6
6
  syncCommand,
7
7
  uninstallCommand
8
- } from "../chunk-HV64EIZU.js";
9
- import "../chunk-K4JQHI7Y.js";
10
- import "../chunk-M5TTM27L.js";
8
+ } from "../chunk-HZ2OIPQ5.js";
9
+ import "../chunk-XKBCLBFT.js";
10
+ import "../chunk-4P6TAP7L.js";
11
11
  import {
12
12
  package_default
13
- } from "../chunk-XO57OJUE.js";
13
+ } from "../chunk-CMGFQCD7.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";