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.
@@ -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.17",
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'`,
@@ -687,15 +688,17 @@ var PostgresClient = class {
687
688
  /**
688
689
  * Create object run entries for a sync run.
689
690
  * All objects start as 'pending'.
691
+ *
692
+ * @param resourceNames - Database resource names (e.g. 'products', 'customers', NOT 'product', 'customer')
690
693
  */
691
- async createObjectRuns(accountId, runStartedAt, objects) {
692
- if (objects.length === 0) return;
693
- const values = objects.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
694
+ async createObjectRuns(accountId, runStartedAt, resourceNames) {
695
+ if (resourceNames.length === 0) return;
696
+ const values = resourceNames.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
694
697
  await this.query(
695
698
  `INSERT INTO "${this.config.schema}"."_sync_obj_runs" ("_account_id", run_started_at, object)
696
699
  VALUES ${values}
697
700
  ON CONFLICT ("_account_id", run_started_at, object) DO NOTHING`,
698
- [accountId, runStartedAt, ...objects]
701
+ [accountId, runStartedAt, ...resourceNames]
699
702
  );
700
703
  }
701
704
  /**
@@ -724,7 +727,7 @@ var PostgresClient = class {
724
727
  */
725
728
  async getObjectRun(accountId, runStartedAt, object) {
726
729
  const result = await this.query(
727
- `SELECT object, status, processed_count, cursor
730
+ `SELECT object, status, processed_count, cursor, page_cursor
728
731
  FROM "${this.config.schema}"."_sync_obj_runs"
729
732
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
730
733
  [accountId, runStartedAt, object]
@@ -735,7 +738,8 @@ var PostgresClient = class {
735
738
  object: row.object,
736
739
  status: row.status,
737
740
  processedCount: row.processed_count,
738
- cursor: row.cursor
741
+ cursor: row.cursor,
742
+ pageCursor: row.page_cursor
739
743
  };
740
744
  }
741
745
  /**
@@ -750,6 +754,23 @@ var PostgresClient = class {
750
754
  [accountId, runStartedAt, object, count]
751
755
  );
752
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
+ }
753
774
  /**
754
775
  * Update the cursor for an object sync.
755
776
  * Only updates if the new cursor is higher than the existing one (cursors should never decrease).
@@ -782,9 +803,10 @@ var PostgresClient = class {
782
803
  }
783
804
  /**
784
805
  * Get the highest cursor from previous syncs for an object type.
785
- * This considers completed, error, AND running runs to ensure recovery syncs
786
- * don't re-process data that was already synced before a crash.
787
- * 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).
788
810
  *
789
811
  * Handles two cursor formats:
790
812
  * - Numeric: compared as bigint for correct ordering
@@ -799,11 +821,31 @@ var PostgresClient = class {
799
821
  FROM "${this.config.schema}"."_sync_obj_runs" o
800
822
  WHERE o."_account_id" = $1
801
823
  AND o.object = $2
802
- AND o.cursor IS NOT NULL`,
824
+ AND o.cursor IS NOT NULL
825
+ AND o.status = 'complete'`,
803
826
  [accountId, object]
804
827
  );
805
828
  return result.rows[0]?.cursor ?? null;
806
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
+ }
807
849
  /**
808
850
  * Delete all sync runs and object runs for an account.
809
851
  * Useful for testing or resetting sync state.
@@ -824,7 +866,7 @@ var PostgresClient = class {
824
866
  async completeObjectSync(accountId, runStartedAt, object) {
825
867
  await this.query(
826
868
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
827
- SET status = 'complete', completed_at = now()
869
+ SET status = 'complete', completed_at = now(), page_cursor = NULL
828
870
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
829
871
  [accountId, runStartedAt, object]
830
872
  );
@@ -840,7 +882,7 @@ var PostgresClient = class {
840
882
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
841
883
  await this.query(
842
884
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
843
- SET status = 'error', error_message = $4, completed_at = now()
885
+ SET status = 'error', error_message = $4, completed_at = now(), page_cursor = NULL
844
886
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
845
887
  [accountId, runStartedAt, object, errorMessage]
846
888
  );
@@ -2141,57 +2183,74 @@ var StripeSync = class {
2141
2183
  * ```
2142
2184
  */
2143
2185
  async processNext(object, params) {
2144
- await this.getCurrentAccount();
2145
- const accountId = await this.getAccountId();
2146
- const resourceName = this.getResourceName(object);
2147
- let runStartedAt;
2148
- if (params?.runStartedAt) {
2149
- runStartedAt = params.runStartedAt;
2150
- } else {
2151
- const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
2152
- runStartedAt = runKey.runStartedAt;
2153
- }
2154
- await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
2155
- const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
2156
- if (objRun?.status === "complete" || objRun?.status === "error") {
2157
- return {
2158
- processed: 0,
2159
- hasMore: false,
2160
- runStartedAt
2161
- };
2162
- }
2163
- if (objRun?.status === "pending") {
2164
- const started = await this.postgresClient.tryStartObjectSync(
2165
- accountId,
2166
- runStartedAt,
2167
- resourceName
2168
- );
2169
- 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") {
2170
2200
  return {
2171
2201
  processed: 0,
2172
- hasMore: true,
2202
+ hasMore: false,
2173
2203
  runStartedAt
2174
2204
  };
2175
2205
  }
2176
- }
2177
- let cursor = null;
2178
- if (!params?.created) {
2179
- if (objRun?.cursor) {
2180
- cursor = objRun.cursor;
2181
- } else {
2182
- 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
+ );
2183
2227
  cursor = lastCursor ?? null;
2184
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);
2185
2241
  }
2186
- const result = await this.fetchOnePage(
2187
- object,
2188
- accountId,
2189
- resourceName,
2190
- runStartedAt,
2191
- cursor,
2192
- params
2193
- );
2194
- 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)));
2195
2254
  }
2196
2255
  /**
2197
2256
  * Get the database resource name for a SyncObject type
@@ -2223,7 +2282,7 @@ var StripeSync = class {
2223
2282
  * Uses resourceRegistry for DRY list/upsert operations.
2224
2283
  * Uses the observable sync system for tracking progress.
2225
2284
  */
2226
- async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, params) {
2285
+ async fetchOnePage(object, accountId, resourceName, runStartedAt, cursor, pageCursor, params) {
2227
2286
  const limit = 100;
2228
2287
  if (object === "payment_method" || object === "tax_id") {
2229
2288
  this.config.logger?.warn(`processNext for ${object} requires customer context`);
@@ -2251,7 +2310,16 @@ var StripeSync = class {
2251
2310
  listParams.created = created;
2252
2311
  }
2253
2312
  }
2313
+ if (pageCursor) {
2314
+ listParams.starting_after = pageCursor;
2315
+ }
2254
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
+ }
2255
2323
  if (response.data.length > 0) {
2256
2324
  this.config.logger?.info(`processNext: upserting ${response.data.length} ${resourceName}`);
2257
2325
  await config.upsertFn(response.data, accountId, params?.backfillRelatedEntities);
@@ -2272,6 +2340,15 @@ var StripeSync = class {
2272
2340
  String(maxCreated)
2273
2341
  );
2274
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
+ }
2275
2352
  }
2276
2353
  if (!response.has_more) {
2277
2354
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
@@ -2420,31 +2497,43 @@ var StripeSync = class {
2420
2497
  * This is used by workers and background processes that should cooperate.
2421
2498
  *
2422
2499
  * @param triggeredBy - What triggered this sync (for observability)
2500
+ * @param objectFilter - Optional specific object to sync (e.g. 'payment_intent'). If 'all' or undefined, syncs all objects.
2423
2501
  * @returns Run key and list of objects to sync
2424
2502
  */
2425
- async joinOrCreateSyncRun(triggeredBy = "worker") {
2503
+ async joinOrCreateSyncRun(triggeredBy = "worker", objectFilter) {
2426
2504
  await this.getCurrentAccount();
2427
2505
  const accountId = await this.getAccountId();
2428
2506
  const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
2507
+ const objects = objectFilter === "all" || objectFilter === void 0 ? this.getSupportedSyncObjects() : [objectFilter];
2429
2508
  if (!result) {
2430
2509
  const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
2431
2510
  if (!activeRun) {
2432
2511
  throw new Error("Failed to get or create sync run");
2433
2512
  }
2513
+ await this.postgresClient.createObjectRuns(
2514
+ activeRun.accountId,
2515
+ activeRun.runStartedAt,
2516
+ objects.map((obj) => this.getResourceName(obj))
2517
+ );
2434
2518
  return {
2435
2519
  runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
2436
- objects: this.getSupportedSyncObjects()
2520
+ objects
2437
2521
  };
2438
2522
  }
2439
2523
  const { accountId: runAccountId, runStartedAt } = result;
2524
+ await this.postgresClient.createObjectRuns(
2525
+ runAccountId,
2526
+ runStartedAt,
2527
+ objects.map((obj) => this.getResourceName(obj))
2528
+ );
2440
2529
  return {
2441
2530
  runKey: { accountId: runAccountId, runStartedAt },
2442
- objects: this.getSupportedSyncObjects()
2531
+ objects
2443
2532
  };
2444
2533
  }
2445
2534
  async processUntilDone(params) {
2446
2535
  const { object } = params ?? { object: "all" };
2447
- const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
2536
+ const { runKey } = await this.joinOrCreateSyncRun("processUntilDone", object);
2448
2537
  return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
2449
2538
  }
2450
2539
  /**
package/dist/cli/index.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  migrateCommand,
6
6
  syncCommand,
7
7
  uninstallCommand
8
- } from "../chunk-YXRCT3RK.js";
9
- import "../chunk-TV67ZOCK.js";
10
- import "../chunk-I7IFXSAU.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-57SXDCMH.js";
13
+ } from "../chunk-CMGFQCD7.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";