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/dist/cli/index.cjs
CHANGED
|
@@ -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.
|
|
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,
|
|
692
|
-
if (
|
|
693
|
-
const values =
|
|
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, ...
|
|
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
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
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
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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:
|
|
2202
|
+
hasMore: false,
|
|
2173
2203
|
runStartedAt
|
|
2174
2204
|
};
|
|
2175
2205
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
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
|
|
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
|
|
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-
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
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-
|
|
13
|
+
} from "../chunk-CMGFQCD7.js";
|
|
14
14
|
|
|
15
15
|
// src/cli/index.ts
|
|
16
16
|
import { Command } from "commander";
|