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 +3 -0
- package/dist/{chunk-I7IFXSAU.js → chunk-4P6TAP7L.js} +1 -1
- package/dist/{chunk-57SXDCMH.js → chunk-CMGFQCD7.js} +1 -1
- package/dist/{chunk-YXRCT3RK.js → chunk-HZ2OIPQ5.js} +2 -2
- package/dist/{chunk-TV67ZOCK.js → chunk-XKBCLBFT.js} +151 -62
- package/dist/cli/index.cjs +151 -62
- package/dist/cli/index.js +4 -4
- package/dist/cli/lib.cjs +151 -62
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +151 -62
- package/dist/index.d.cts +23 -5
- package/dist/index.d.ts +23 -5
- package/dist/index.js +2 -2
- package/dist/migrations/0061_add_page_cursor.sql +3 -0
- package/dist/supabase/index.cjs +1 -1
- package/dist/supabase/index.js +2 -2
- package/package.json +1 -1
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
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
StripeSync,
|
|
3
3
|
createStripeWebSocketClient,
|
|
4
4
|
runMigrations
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XKBCLBFT.js";
|
|
6
6
|
import {
|
|
7
7
|
install,
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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,
|
|
510
|
-
if (
|
|
511
|
-
const values =
|
|
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, ...
|
|
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
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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:
|
|
2020
|
+
hasMore: false,
|
|
1991
2021
|
runStartedAt
|
|
1992
2022
|
};
|
|
1993
2023
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
|
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
|
|
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
|
/**
|