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/dist/cli/lib.cjs CHANGED
@@ -117,7 +117,7 @@ async function loadConfig(options) {
117
117
  // package.json
118
118
  var package_default = {
119
119
  name: "stripe-experiment-sync",
120
- version: "1.0.17",
120
+ version: "1.0.19",
121
121
  private: false,
122
122
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
123
123
  type: "module",
@@ -594,7 +594,8 @@ var PostgresClient = class {
594
594
  `UPDATE "${this.config.schema}"."_sync_obj_runs" o
595
595
  SET status = 'error',
596
596
  error_message = 'Auto-cancelled: stale (no update in 5 min)',
597
- completed_at = now()
597
+ completed_at = now(),
598
+ page_cursor = NULL
598
599
  WHERE o."_account_id" = $1
599
600
  AND o.status = 'running'
600
601
  AND o.updated_at < now() - interval '5 minutes'`,
@@ -701,15 +702,17 @@ var PostgresClient = class {
701
702
  /**
702
703
  * Create object run entries for a sync run.
703
704
  * All objects start as 'pending'.
705
+ *
706
+ * @param resourceNames - Database resource names (e.g. 'products', 'customers', NOT 'product', 'customer')
704
707
  */
705
- async createObjectRuns(accountId, runStartedAt, objects) {
706
- if (objects.length === 0) return;
707
- const values = objects.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
708
+ async createObjectRuns(accountId, runStartedAt, resourceNames) {
709
+ if (resourceNames.length === 0) return;
710
+ const values = resourceNames.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
708
711
  await this.query(
709
712
  `INSERT INTO "${this.config.schema}"."_sync_obj_runs" ("_account_id", run_started_at, object)
710
713
  VALUES ${values}
711
714
  ON CONFLICT ("_account_id", run_started_at, object) DO NOTHING`,
712
- [accountId, runStartedAt, ...objects]
715
+ [accountId, runStartedAt, ...resourceNames]
713
716
  );
714
717
  }
715
718
  /**
@@ -738,7 +741,7 @@ var PostgresClient = class {
738
741
  */
739
742
  async getObjectRun(accountId, runStartedAt, object) {
740
743
  const result = await this.query(
741
- `SELECT object, status, processed_count, cursor
744
+ `SELECT object, status, processed_count, cursor, page_cursor
742
745
  FROM "${this.config.schema}"."_sync_obj_runs"
743
746
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
744
747
  [accountId, runStartedAt, object]
@@ -749,7 +752,8 @@ var PostgresClient = class {
749
752
  object: row.object,
750
753
  status: row.status,
751
754
  processedCount: row.processed_count,
752
- cursor: row.cursor
755
+ cursor: row.cursor,
756
+ pageCursor: row.page_cursor
753
757
  };
754
758
  }
755
759
  /**
@@ -764,6 +768,23 @@ var PostgresClient = class {
764
768
  [accountId, runStartedAt, object, count]
765
769
  );
766
770
  }
771
+ /**
772
+ * Update the pagination page_cursor used for backfills using Stripe list calls.
773
+ */
774
+ async updateObjectPageCursor(accountId, runStartedAt, object, pageCursor) {
775
+ await this.query(
776
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
777
+ SET page_cursor = $4, updated_at = now()
778
+ WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
779
+ [accountId, runStartedAt, object, pageCursor]
780
+ );
781
+ }
782
+ /**
783
+ * Clear the pagination page_cursor for an object sync.
784
+ */
785
+ async clearObjectPageCursor(accountId, runStartedAt, object) {
786
+ await this.updateObjectPageCursor(accountId, runStartedAt, object, null);
787
+ }
767
788
  /**
768
789
  * Update the cursor for an object sync.
769
790
  * Only updates if the new cursor is higher than the existing one (cursors should never decrease).
@@ -796,9 +817,10 @@ var PostgresClient = class {
796
817
  }
797
818
  /**
798
819
  * Get the highest cursor from previous syncs for an object type.
799
- * This considers completed, error, AND running runs to ensure recovery syncs
800
- * don't re-process data that was already synced before a crash.
801
- * A 'running' status with a cursor means the process was killed mid-sync.
820
+ * Uses only completed object runs.
821
+ * - During the initial backfill we page through history, but we also update the cursor as we go.
822
+ * If we crash mid-backfill and reuse that cursor, we can accidentally switch into incremental mode
823
+ * too early and only ever fetch the newest page (breaking the historical backfill).
802
824
  *
803
825
  * Handles two cursor formats:
804
826
  * - Numeric: compared as bigint for correct ordering
@@ -813,11 +835,31 @@ var PostgresClient = class {
813
835
  FROM "${this.config.schema}"."_sync_obj_runs" o
814
836
  WHERE o."_account_id" = $1
815
837
  AND o.object = $2
816
- AND o.cursor IS NOT NULL`,
838
+ AND o.cursor IS NOT NULL
839
+ AND o.status = 'complete'`,
817
840
  [accountId, object]
818
841
  );
819
842
  return result.rows[0]?.cursor ?? null;
820
843
  }
844
+ /**
845
+ * Get the highest cursor from previous syncs for an object type, excluding the current run.
846
+ */
847
+ async getLastCursorBeforeRun(accountId, object, runStartedAt) {
848
+ const result = await this.query(
849
+ `SELECT CASE
850
+ WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
851
+ ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
852
+ END as cursor
853
+ FROM "${this.config.schema}"."_sync_obj_runs" o
854
+ WHERE o."_account_id" = $1
855
+ AND o.object = $2
856
+ AND o.cursor IS NOT NULL
857
+ AND o.status = 'complete'
858
+ AND o.run_started_at < $3`,
859
+ [accountId, object, runStartedAt]
860
+ );
861
+ return result.rows[0]?.cursor ?? null;
862
+ }
821
863
  /**
822
864
  * Delete all sync runs and object runs for an account.
823
865
  * Useful for testing or resetting sync state.
@@ -838,7 +880,7 @@ var PostgresClient = class {
838
880
  async completeObjectSync(accountId, runStartedAt, object) {
839
881
  await this.query(
840
882
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
841
- SET status = 'complete', completed_at = now()
883
+ SET status = 'complete', completed_at = now(), page_cursor = NULL
842
884
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
843
885
  [accountId, runStartedAt, object]
844
886
  );
@@ -854,7 +896,7 @@ var PostgresClient = class {
854
896
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
855
897
  await this.query(
856
898
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
857
- SET status = 'error', error_message = $4, completed_at = now()
899
+ SET status = 'error', error_message = $4, completed_at = now(), page_cursor = NULL
858
900
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
859
901
  [accountId, runStartedAt, object, errorMessage]
860
902
  );
@@ -2155,57 +2197,74 @@ var StripeSync = class {
2155
2197
  * ```
2156
2198
  */
2157
2199
  async processNext(object, params) {
2158
- await this.getCurrentAccount();
2159
- const accountId = await this.getAccountId();
2160
- const resourceName = this.getResourceName(object);
2161
- let runStartedAt;
2162
- if (params?.runStartedAt) {
2163
- runStartedAt = params.runStartedAt;
2164
- } else {
2165
- const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
2166
- runStartedAt = runKey.runStartedAt;
2167
- }
2168
- await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2169
- const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
2170
- if (objRun?.status === "complete" || objRun?.status === "error") {
2171
- return {
2172
- processed: 0,
2173
- hasMore: false,
2174
- runStartedAt
2175
- };
2176
- }
2177
- if (objRun?.status === "pending") {
2178
- const started = await this.postgresClient.tryStartObjectSync(
2179
- accountId,
2180
- runStartedAt,
2181
- resourceName
2182
- );
2183
- if (!started) {
2200
+ try {
2201
+ await this.getCurrentAccount();
2202
+ const accountId = await this.getAccountId();
2203
+ const resourceName = this.getResourceName(object);
2204
+ let runStartedAt;
2205
+ if (params?.runStartedAt) {
2206
+ runStartedAt = params.runStartedAt;
2207
+ } else {
2208
+ const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
2209
+ runStartedAt = runKey.runStartedAt;
2210
+ }
2211
+ await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2212
+ const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
2213
+ if (objRun?.status === "complete" || objRun?.status === "error") {
2184
2214
  return {
2185
2215
  processed: 0,
2186
- hasMore: true,
2216
+ hasMore: false,
2187
2217
  runStartedAt
2188
2218
  };
2189
2219
  }
2190
- }
2191
- let cursor = null;
2192
- if (!params?.created) {
2193
- if (objRun?.cursor) {
2194
- cursor = objRun.cursor;
2195
- } else {
2196
- const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
2220
+ if (objRun?.status === "pending") {
2221
+ const started = await this.postgresClient.tryStartObjectSync(
2222
+ accountId,
2223
+ runStartedAt,
2224
+ resourceName
2225
+ );
2226
+ if (!started) {
2227
+ return {
2228
+ processed: 0,
2229
+ hasMore: true,
2230
+ runStartedAt
2231
+ };
2232
+ }
2233
+ }
2234
+ let cursor = null;
2235
+ if (!params?.created) {
2236
+ const lastCursor = await this.postgresClient.getLastCursorBeforeRun(
2237
+ accountId,
2238
+ resourceName,
2239
+ runStartedAt
2240
+ );
2197
2241
  cursor = lastCursor ?? null;
2198
2242
  }
2243
+ const result = await this.fetchOnePage(
2244
+ object,
2245
+ accountId,
2246
+ resourceName,
2247
+ runStartedAt,
2248
+ cursor,
2249
+ objRun?.pageCursor ?? null,
2250
+ params
2251
+ );
2252
+ return result;
2253
+ } catch (error) {
2254
+ throw this.appendMigrationHint(error);
2199
2255
  }
2200
- const result = await this.fetchOnePage(
2201
- object,
2202
- accountId,
2203
- resourceName,
2204
- runStartedAt,
2205
- cursor,
2206
- params
2207
- );
2208
- return result;
2256
+ }
2257
+ appendMigrationHint(error) {
2258
+ const hint = "Error occurred. Make sure you are up to date with DB migrations which can sometimes help with this. Details:";
2259
+ const withHint = (message) => message.includes(hint) ? message : `${hint}
2260
+ ${message}`;
2261
+ if (error instanceof Error) {
2262
+ const { stack } = error;
2263
+ error.message = withHint(error.message);
2264
+ if (stack) error.stack = stack;
2265
+ return error;
2266
+ }
2267
+ return new Error(withHint(String(error)));
2209
2268
  }
2210
2269
  /**
2211
2270
  * Get the database resource name for a SyncObject type
@@ -2237,7 +2296,7 @@ var StripeSync = class {
2237
2296
  * Uses resourceRegistry for DRY list/upsert operations.
2238
2297
  * Uses the observable sync system for tracking progress.
2239
2298
  */
2240
- async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, params) {
2299
+ async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
2241
2300
  const limit = 100;
2242
2301
  if (object === "payment_method" || object === "tax_id") {
2243
2302
  this.config.logger?.warn(`processNext for ${object} requires customer context`);
@@ -2265,7 +2324,16 @@ var StripeSync = class {
2265
2324
  listParams.created = created;
2266
2325
  }
2267
2326
  }
2327
+ if (pageCursor) {
2328
+ listParams.starting_after = pageCursor;
2329
+ }
2268
2330
  const response = await config.listFn(listParams);
2331
+ if (response.data.length === 0 && response.has_more) {
2332
+ const message = `Stripe returned has_more=true with empty page for ${resourceName}. Aborting to avoid infinite loop.`;
2333
+ this.config.logger?.warn(message);
2334
+ await this.postgresClient.failObjectSync(accountId, runStartedAt, resourceName, message);
2335
+ return { processed: 0, hasMore: false, runStartedAt };
2336
+ }
2269
2337
  if (response.data.length > 0) {
2270
2338
  this.config.logger?.info(`processNext: upserting ${response.data.length} ${resourceName}`);
2271
2339
  await config.upsertFn(response.data, accountId, params?.backfillRelatedEntities);
@@ -2286,6 +2354,15 @@ var StripeSync = class {
2286
2354
  String(maxCreated)
2287
2355
  );
2288
2356
  }
2357
+ const lastId = response.data[response.data.length - 1].id;
2358
+ if (response.has_more) {
2359
+ await this.postgresClient.updateObjectPageCursor(
2360
+ accountId,
2361
+ runStartedAt,
2362
+ resourceName,
2363
+ lastId
2364
+ );
2365
+ }
2289
2366
  }
2290
2367
  if (!response.has_more) {
2291
2368
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
@@ -2434,31 +2511,43 @@ var StripeSync = class {
2434
2511
  * This is used by workers and background processes that should cooperate.
2435
2512
  *
2436
2513
  * @param triggeredBy - What triggered this sync (for observability)
2514
+ * @param objectFilter - Optional specific object to sync (e.g. 'payment_intent'). If 'all' or undefined, syncs all objects.
2437
2515
  * @returns Run key and list of objects to sync
2438
2516
  */
2439
- async joinOrCreateSyncRun(triggeredBy = "worker") {
2517
+ async joinOrCreateSyncRun(triggeredBy = "worker", objectFilter) {
2440
2518
  await this.getCurrentAccount();
2441
2519
  const accountId = await this.getAccountId();
2442
2520
  const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
2521
+ const objects = objectFilter === "all" || objectFilter === void 0 ? this.getSupportedSyncObjects() : [objectFilter];
2443
2522
  if (!result) {
2444
2523
  const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
2445
2524
  if (!activeRun) {
2446
2525
  throw new Error("Failed to get or create sync run");
2447
2526
  }
2527
+ await this.postgresClient.createObjectRuns(
2528
+ activeRun.accountId,
2529
+ activeRun.runStartedAt,
2530
+ objects.map((obj) => this.getResourceName(obj))
2531
+ );
2448
2532
  return {
2449
2533
  runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
2450
- objects: this.getSupportedSyncObjects()
2534
+ objects
2451
2535
  };
2452
2536
  }
2453
2537
  const { accountId: runAccountId, runStartedAt } = result;
2538
+ await this.postgresClient.createObjectRuns(
2539
+ runAccountId,
2540
+ runStartedAt,
2541
+ objects.map((obj) => this.getResourceName(obj))
2542
+ );
2454
2543
  return {
2455
2544
  runKey: { accountId: runAccountId, runStartedAt },
2456
- objects: this.getSupportedSyncObjects()
2545
+ objects
2457
2546
  };
2458
2547
  }
2459
2548
  async processUntilDone(params) {
2460
2549
  const { object } = params ?? { object: "all" };
2461
- const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
2550
+ const { runKey } = await this.joinOrCreateSyncRun("processUntilDone", object);
2462
2551
  return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
2463
2552
  }
2464
2553
  /**
package/dist/cli/lib.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  migrateCommand,
7
7
  syncCommand,
8
8
  uninstallCommand
9
- } from "../chunk-YXRCT3RK.js";
10
- import "../chunk-TV67ZOCK.js";
11
- import "../chunk-I7IFXSAU.js";
12
- import "../chunk-57SXDCMH.js";
9
+ } from "../chunk-HZ2OIPQ5.js";
10
+ import "../chunk-XKBCLBFT.js";
11
+ import "../chunk-4P6TAP7L.js";
12
+ import "../chunk-CMGFQCD7.js";
13
13
  export {
14
14
  backfillCommand,
15
15
  createTunnel,