stripe-experiment-sync 1.0.9 → 1.0.11
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/{chunk-PWJLHHPY.js → chunk-4MOFTZ53.js} +4 -3
- package/dist/{chunk-OLVA37VZ.js → chunk-EVNKMOYA.js} +67 -22
- package/dist/{chunk-PQ2T7XTY.js → chunk-LFXGDVMC.js} +1 -1
- package/dist/{chunk-RR5BGG4F.js → chunk-XTL72K7K.js} +1 -1
- package/dist/cli/index.cjs +75 -24
- package/dist/cli/index.js +10 -5
- package/dist/cli/lib.cjs +69 -23
- package/dist/cli/lib.d.cts +1 -0
- package/dist/cli/lib.d.ts +1 -0
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +2 -2
- package/dist/supabase/index.cjs +67 -22
- package/dist/supabase/index.d.cts +4 -2
- package/dist/supabase/index.d.ts +4 -2
- package/dist/supabase/index.js +2 -2
- package/package.json +1 -1
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
StripeSync,
|
|
3
3
|
createStripeWebSocketClient,
|
|
4
4
|
runMigrations
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-LFXGDVMC.js";
|
|
6
6
|
import {
|
|
7
7
|
install,
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-EVNKMOYA.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/config.ts
|
|
12
12
|
import dotenv from "dotenv";
|
|
@@ -533,7 +533,8 @@ async function installCommand(options) {
|
|
|
533
533
|
supabaseAccessToken: accessToken,
|
|
534
534
|
supabaseProjectRef: projectRef,
|
|
535
535
|
stripeKey,
|
|
536
|
-
packageVersion: options.packageVersion
|
|
536
|
+
packageVersion: options.packageVersion,
|
|
537
|
+
workerIntervalSeconds: options.workerInterval
|
|
537
538
|
});
|
|
538
539
|
console.log(chalk3.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
539
540
|
console.log(chalk3.cyan.bold(" Installation Complete!"));
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
package_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XTL72K7K.js";
|
|
4
4
|
|
|
5
5
|
// src/supabase/supabase.ts
|
|
6
6
|
import { SupabaseManagementAPI } from "supabase-management-js";
|
|
7
7
|
|
|
8
|
-
// raw-ts:/
|
|
9
|
-
var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method
|
|
8
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
9
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
10
10
|
|
|
11
|
-
// raw-ts:/
|
|
11
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
12
12
|
var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
13
13
|
|
|
14
|
-
// raw-ts:/
|
|
15
|
-
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron
|
|
14
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
15
|
+
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
16
16
|
|
|
17
17
|
// src/supabase/edge-function-code.ts
|
|
18
18
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -86,8 +86,29 @@ var SupabaseSetupClient = class {
|
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Setup pg_cron job to invoke worker function
|
|
89
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
89
90
|
*/
|
|
90
|
-
async setupPgCronJob() {
|
|
91
|
+
async setupPgCronJob(intervalSeconds = 60) {
|
|
92
|
+
if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
|
|
93
|
+
throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
|
|
94
|
+
}
|
|
95
|
+
let schedule;
|
|
96
|
+
if (intervalSeconds < 60) {
|
|
97
|
+
schedule = `${intervalSeconds} seconds`;
|
|
98
|
+
} else if (intervalSeconds % 60 === 0) {
|
|
99
|
+
const minutes = intervalSeconds / 60;
|
|
100
|
+
if (minutes < 60) {
|
|
101
|
+
schedule = `*/${minutes} * * * *`;
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
91
112
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
92
113
|
const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
|
|
93
114
|
const sql = `
|
|
@@ -115,11 +136,11 @@ var SupabaseSetupClient = class {
|
|
|
115
136
|
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
|
|
116
137
|
);
|
|
117
138
|
|
|
118
|
-
-- Create job to invoke worker
|
|
139
|
+
-- Create job to invoke worker at configured interval
|
|
119
140
|
-- Worker reads from pgmq, enqueues objects if empty, and processes sync work
|
|
120
141
|
SELECT cron.schedule(
|
|
121
142
|
'stripe-sync-worker',
|
|
122
|
-
'
|
|
143
|
+
'${schedule}',
|
|
123
144
|
$$
|
|
124
145
|
SELECT net.http_post(
|
|
125
146
|
url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
|
|
@@ -303,10 +324,12 @@ var SupabaseSetupClient = class {
|
|
|
303
324
|
await this.deleteSecret("STRIPE_SECRET_KEY");
|
|
304
325
|
try {
|
|
305
326
|
await this.runSQL(`
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
|
|
309
|
-
|
|
327
|
+
DO $$
|
|
328
|
+
BEGIN
|
|
329
|
+
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
|
|
330
|
+
PERFORM cron.unschedule('stripe-sync-worker');
|
|
331
|
+
END IF;
|
|
332
|
+
END $$;
|
|
310
333
|
`);
|
|
311
334
|
} catch (err) {
|
|
312
335
|
console.warn("Could not unschedule pg_cron job:", err);
|
|
@@ -322,15 +345,31 @@ var SupabaseSetupClient = class {
|
|
|
322
345
|
try {
|
|
323
346
|
await this.runSQL(`
|
|
324
347
|
SELECT pg_terminate_backend(pid)
|
|
325
|
-
FROM
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
348
|
+
FROM pg_locks l
|
|
349
|
+
JOIN pg_class c ON l.relation = c.oid
|
|
350
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
351
|
+
WHERE n.nspname = 'stripe'
|
|
352
|
+
AND l.pid != pg_backend_pid()
|
|
329
353
|
`);
|
|
330
354
|
} catch (err) {
|
|
331
355
|
console.warn("Could not terminate connections:", err);
|
|
332
356
|
}
|
|
333
|
-
|
|
357
|
+
let dropAttempts = 0;
|
|
358
|
+
const maxAttempts = 3;
|
|
359
|
+
while (dropAttempts < maxAttempts) {
|
|
360
|
+
try {
|
|
361
|
+
await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
|
|
362
|
+
break;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
dropAttempts++;
|
|
365
|
+
if (dropAttempts >= maxAttempts) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
334
373
|
} catch (error) {
|
|
335
374
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
336
375
|
}
|
|
@@ -347,7 +386,7 @@ var SupabaseSetupClient = class {
|
|
|
347
386
|
`from 'npm:stripe-experiment-sync@${version}'`
|
|
348
387
|
);
|
|
349
388
|
}
|
|
350
|
-
async install(stripeKey, packageVersion) {
|
|
389
|
+
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
|
351
390
|
const trimmedStripeKey = stripeKey.trim();
|
|
352
391
|
if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
353
392
|
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
@@ -371,7 +410,7 @@ var SupabaseSetupClient = class {
|
|
|
371
410
|
if (!setupResult.success) {
|
|
372
411
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
373
412
|
}
|
|
374
|
-
await this.setupPgCronJob();
|
|
413
|
+
await this.setupPgCronJob(workerIntervalSeconds);
|
|
375
414
|
await this.updateInstallationComment(
|
|
376
415
|
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
|
|
377
416
|
);
|
|
@@ -384,14 +423,20 @@ var SupabaseSetupClient = class {
|
|
|
384
423
|
}
|
|
385
424
|
};
|
|
386
425
|
async function install(params) {
|
|
387
|
-
const {
|
|
426
|
+
const {
|
|
427
|
+
supabaseAccessToken,
|
|
428
|
+
supabaseProjectRef,
|
|
429
|
+
stripeKey,
|
|
430
|
+
packageVersion,
|
|
431
|
+
workerIntervalSeconds
|
|
432
|
+
} = params;
|
|
388
433
|
const client = new SupabaseSetupClient({
|
|
389
434
|
accessToken: supabaseAccessToken,
|
|
390
435
|
projectRef: supabaseProjectRef,
|
|
391
436
|
projectBaseUrl: params.baseProjectUrl,
|
|
392
437
|
managementApiBaseUrl: params.baseManagementApiUrl
|
|
393
438
|
});
|
|
394
|
-
await client.install(stripeKey, packageVersion);
|
|
439
|
+
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
395
440
|
}
|
|
396
441
|
async function uninstall(params) {
|
|
397
442
|
const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
|
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.11",
|
|
37
37
|
private: false,
|
|
38
38
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
39
39
|
type: "module",
|
|
@@ -3630,14 +3630,14 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
3630
3630
|
// src/supabase/supabase.ts
|
|
3631
3631
|
var import_supabase_management_js = require("supabase-management-js");
|
|
3632
3632
|
|
|
3633
|
-
// raw-ts:/
|
|
3634
|
-
var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method
|
|
3633
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
3634
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
3635
3635
|
|
|
3636
|
-
// raw-ts:/
|
|
3636
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
3637
3637
|
var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
3638
3638
|
|
|
3639
|
-
// raw-ts:/
|
|
3640
|
-
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron
|
|
3639
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
3640
|
+
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
3641
3641
|
|
|
3642
3642
|
// src/supabase/edge-function-code.ts
|
|
3643
3643
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -3711,8 +3711,29 @@ var SupabaseSetupClient = class {
|
|
|
3711
3711
|
}
|
|
3712
3712
|
/**
|
|
3713
3713
|
* Setup pg_cron job to invoke worker function
|
|
3714
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
3714
3715
|
*/
|
|
3715
|
-
async setupPgCronJob() {
|
|
3716
|
+
async setupPgCronJob(intervalSeconds = 60) {
|
|
3717
|
+
if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
|
|
3718
|
+
throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
|
|
3719
|
+
}
|
|
3720
|
+
let schedule;
|
|
3721
|
+
if (intervalSeconds < 60) {
|
|
3722
|
+
schedule = `${intervalSeconds} seconds`;
|
|
3723
|
+
} else if (intervalSeconds % 60 === 0) {
|
|
3724
|
+
const minutes = intervalSeconds / 60;
|
|
3725
|
+
if (minutes < 60) {
|
|
3726
|
+
schedule = `*/${minutes} * * * *`;
|
|
3727
|
+
} else {
|
|
3728
|
+
throw new Error(
|
|
3729
|
+
`Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
} else {
|
|
3733
|
+
throw new Error(
|
|
3734
|
+
`Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3716
3737
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
3717
3738
|
const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
|
|
3718
3739
|
const sql3 = `
|
|
@@ -3740,11 +3761,11 @@ var SupabaseSetupClient = class {
|
|
|
3740
3761
|
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
|
|
3741
3762
|
);
|
|
3742
3763
|
|
|
3743
|
-
-- Create job to invoke worker
|
|
3764
|
+
-- Create job to invoke worker at configured interval
|
|
3744
3765
|
-- Worker reads from pgmq, enqueues objects if empty, and processes sync work
|
|
3745
3766
|
SELECT cron.schedule(
|
|
3746
3767
|
'stripe-sync-worker',
|
|
3747
|
-
'
|
|
3768
|
+
'${schedule}',
|
|
3748
3769
|
$$
|
|
3749
3770
|
SELECT net.http_post(
|
|
3750
3771
|
url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
|
|
@@ -3928,10 +3949,12 @@ var SupabaseSetupClient = class {
|
|
|
3928
3949
|
await this.deleteSecret("STRIPE_SECRET_KEY");
|
|
3929
3950
|
try {
|
|
3930
3951
|
await this.runSQL(`
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
|
|
3934
|
-
|
|
3952
|
+
DO $$
|
|
3953
|
+
BEGIN
|
|
3954
|
+
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
|
|
3955
|
+
PERFORM cron.unschedule('stripe-sync-worker');
|
|
3956
|
+
END IF;
|
|
3957
|
+
END $$;
|
|
3935
3958
|
`);
|
|
3936
3959
|
} catch (err) {
|
|
3937
3960
|
console.warn("Could not unschedule pg_cron job:", err);
|
|
@@ -3947,15 +3970,31 @@ var SupabaseSetupClient = class {
|
|
|
3947
3970
|
try {
|
|
3948
3971
|
await this.runSQL(`
|
|
3949
3972
|
SELECT pg_terminate_backend(pid)
|
|
3950
|
-
FROM
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3973
|
+
FROM pg_locks l
|
|
3974
|
+
JOIN pg_class c ON l.relation = c.oid
|
|
3975
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
3976
|
+
WHERE n.nspname = 'stripe'
|
|
3977
|
+
AND l.pid != pg_backend_pid()
|
|
3954
3978
|
`);
|
|
3955
3979
|
} catch (err) {
|
|
3956
3980
|
console.warn("Could not terminate connections:", err);
|
|
3957
3981
|
}
|
|
3958
|
-
|
|
3982
|
+
let dropAttempts = 0;
|
|
3983
|
+
const maxAttempts = 3;
|
|
3984
|
+
while (dropAttempts < maxAttempts) {
|
|
3985
|
+
try {
|
|
3986
|
+
await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
|
|
3987
|
+
break;
|
|
3988
|
+
} catch (err) {
|
|
3989
|
+
dropAttempts++;
|
|
3990
|
+
if (dropAttempts >= maxAttempts) {
|
|
3991
|
+
throw new Error(
|
|
3992
|
+
`Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3959
3998
|
} catch (error) {
|
|
3960
3999
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3961
4000
|
}
|
|
@@ -3972,7 +4011,7 @@ var SupabaseSetupClient = class {
|
|
|
3972
4011
|
`from 'npm:stripe-experiment-sync@${version}'`
|
|
3973
4012
|
);
|
|
3974
4013
|
}
|
|
3975
|
-
async install(stripeKey, packageVersion) {
|
|
4014
|
+
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
|
3976
4015
|
const trimmedStripeKey = stripeKey.trim();
|
|
3977
4016
|
if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
3978
4017
|
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
@@ -3996,7 +4035,7 @@ var SupabaseSetupClient = class {
|
|
|
3996
4035
|
if (!setupResult.success) {
|
|
3997
4036
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
3998
4037
|
}
|
|
3999
|
-
await this.setupPgCronJob();
|
|
4038
|
+
await this.setupPgCronJob(workerIntervalSeconds);
|
|
4000
4039
|
await this.updateInstallationComment(
|
|
4001
4040
|
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
|
|
4002
4041
|
);
|
|
@@ -4009,14 +4048,20 @@ var SupabaseSetupClient = class {
|
|
|
4009
4048
|
}
|
|
4010
4049
|
};
|
|
4011
4050
|
async function install(params) {
|
|
4012
|
-
const {
|
|
4051
|
+
const {
|
|
4052
|
+
supabaseAccessToken,
|
|
4053
|
+
supabaseProjectRef,
|
|
4054
|
+
stripeKey,
|
|
4055
|
+
packageVersion,
|
|
4056
|
+
workerIntervalSeconds
|
|
4057
|
+
} = params;
|
|
4013
4058
|
const client = new SupabaseSetupClient({
|
|
4014
4059
|
accessToken: supabaseAccessToken,
|
|
4015
4060
|
projectRef: supabaseProjectRef,
|
|
4016
4061
|
projectBaseUrl: params.baseProjectUrl,
|
|
4017
4062
|
managementApiBaseUrl: params.baseManagementApiUrl
|
|
4018
4063
|
});
|
|
4019
|
-
await client.install(stripeKey, packageVersion);
|
|
4064
|
+
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
4020
4065
|
}
|
|
4021
4066
|
async function uninstall(params) {
|
|
4022
4067
|
const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
|
|
@@ -4467,7 +4512,8 @@ async function installCommand(options) {
|
|
|
4467
4512
|
supabaseAccessToken: accessToken,
|
|
4468
4513
|
supabaseProjectRef: projectRef,
|
|
4469
4514
|
stripeKey,
|
|
4470
|
-
packageVersion: options.packageVersion
|
|
4515
|
+
packageVersion: options.packageVersion,
|
|
4516
|
+
workerIntervalSeconds: options.workerInterval
|
|
4471
4517
|
});
|
|
4472
4518
|
console.log(import_chalk3.default.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
4473
4519
|
console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
|
|
@@ -4585,12 +4631,17 @@ var supabase = program.command("supabase").description("Supabase Edge Functions
|
|
|
4585
4631
|
supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option(
|
|
4586
4632
|
"--package-version <version>",
|
|
4587
4633
|
"Package version to install (e.g., 1.0.8-beta.1, defaults to latest)"
|
|
4634
|
+
).option(
|
|
4635
|
+
"--worker-interval <seconds>",
|
|
4636
|
+
"Worker interval in seconds (defaults to 60)",
|
|
4637
|
+
(val) => parseInt(val, 10)
|
|
4588
4638
|
).action(async (options) => {
|
|
4589
4639
|
await installCommand({
|
|
4590
4640
|
supabaseAccessToken: options.token,
|
|
4591
4641
|
supabaseProjectRef: options.project,
|
|
4592
4642
|
stripeKey: options.stripeKey,
|
|
4593
|
-
packageVersion: options.packageVersion
|
|
4643
|
+
packageVersion: options.packageVersion,
|
|
4644
|
+
workerInterval: options.workerInterval
|
|
4594
4645
|
});
|
|
4595
4646
|
});
|
|
4596
4647
|
supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
|
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-4MOFTZ53.js";
|
|
9
|
+
import "../chunk-LFXGDVMC.js";
|
|
10
|
+
import "../chunk-EVNKMOYA.js";
|
|
11
11
|
import {
|
|
12
12
|
package_default
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-XTL72K7K.js";
|
|
14
14
|
|
|
15
15
|
// src/cli/index.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -41,12 +41,17 @@ var supabase = program.command("supabase").description("Supabase Edge Functions
|
|
|
41
41
|
supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option(
|
|
42
42
|
"--package-version <version>",
|
|
43
43
|
"Package version to install (e.g., 1.0.8-beta.1, defaults to latest)"
|
|
44
|
+
).option(
|
|
45
|
+
"--worker-interval <seconds>",
|
|
46
|
+
"Worker interval in seconds (defaults to 60)",
|
|
47
|
+
(val) => parseInt(val, 10)
|
|
44
48
|
).action(async (options) => {
|
|
45
49
|
await installCommand({
|
|
46
50
|
supabaseAccessToken: options.token,
|
|
47
51
|
supabaseProjectRef: options.project,
|
|
48
52
|
stripeKey: options.stripeKey,
|
|
49
|
-
packageVersion: options.packageVersion
|
|
53
|
+
packageVersion: options.packageVersion,
|
|
54
|
+
workerInterval: options.workerInterval
|
|
50
55
|
});
|
|
51
56
|
});
|
|
52
57
|
supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
|
package/dist/cli/lib.cjs
CHANGED
|
@@ -105,7 +105,7 @@ async function loadConfig(options) {
|
|
|
105
105
|
// package.json
|
|
106
106
|
var package_default = {
|
|
107
107
|
name: "stripe-experiment-sync",
|
|
108
|
-
version: "1.0.
|
|
108
|
+
version: "1.0.11",
|
|
109
109
|
private: false,
|
|
110
110
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
111
111
|
type: "module",
|
|
@@ -3644,14 +3644,14 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
3644
3644
|
// src/supabase/supabase.ts
|
|
3645
3645
|
var import_supabase_management_js = require("supabase-management-js");
|
|
3646
3646
|
|
|
3647
|
-
// raw-ts:/
|
|
3648
|
-
var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method
|
|
3647
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
3648
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
3649
3649
|
|
|
3650
|
-
// raw-ts:/
|
|
3650
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
3651
3651
|
var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
3652
3652
|
|
|
3653
|
-
// raw-ts:/
|
|
3654
|
-
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron
|
|
3653
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
3654
|
+
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
3655
3655
|
|
|
3656
3656
|
// src/supabase/edge-function-code.ts
|
|
3657
3657
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -3725,8 +3725,29 @@ var SupabaseSetupClient = class {
|
|
|
3725
3725
|
}
|
|
3726
3726
|
/**
|
|
3727
3727
|
* Setup pg_cron job to invoke worker function
|
|
3728
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
3728
3729
|
*/
|
|
3729
|
-
async setupPgCronJob() {
|
|
3730
|
+
async setupPgCronJob(intervalSeconds = 60) {
|
|
3731
|
+
if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
|
|
3732
|
+
throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
|
|
3733
|
+
}
|
|
3734
|
+
let schedule;
|
|
3735
|
+
if (intervalSeconds < 60) {
|
|
3736
|
+
schedule = `${intervalSeconds} seconds`;
|
|
3737
|
+
} else if (intervalSeconds % 60 === 0) {
|
|
3738
|
+
const minutes = intervalSeconds / 60;
|
|
3739
|
+
if (minutes < 60) {
|
|
3740
|
+
schedule = `*/${minutes} * * * *`;
|
|
3741
|
+
} else {
|
|
3742
|
+
throw new Error(
|
|
3743
|
+
`Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3746
|
+
} else {
|
|
3747
|
+
throw new Error(
|
|
3748
|
+
`Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3730
3751
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
3731
3752
|
const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
|
|
3732
3753
|
const sql3 = `
|
|
@@ -3754,11 +3775,11 @@ var SupabaseSetupClient = class {
|
|
|
3754
3775
|
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
|
|
3755
3776
|
);
|
|
3756
3777
|
|
|
3757
|
-
-- Create job to invoke worker
|
|
3778
|
+
-- Create job to invoke worker at configured interval
|
|
3758
3779
|
-- Worker reads from pgmq, enqueues objects if empty, and processes sync work
|
|
3759
3780
|
SELECT cron.schedule(
|
|
3760
3781
|
'stripe-sync-worker',
|
|
3761
|
-
'
|
|
3782
|
+
'${schedule}',
|
|
3762
3783
|
$$
|
|
3763
3784
|
SELECT net.http_post(
|
|
3764
3785
|
url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
|
|
@@ -3942,10 +3963,12 @@ var SupabaseSetupClient = class {
|
|
|
3942
3963
|
await this.deleteSecret("STRIPE_SECRET_KEY");
|
|
3943
3964
|
try {
|
|
3944
3965
|
await this.runSQL(`
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
|
|
3948
|
-
|
|
3966
|
+
DO $$
|
|
3967
|
+
BEGIN
|
|
3968
|
+
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
|
|
3969
|
+
PERFORM cron.unschedule('stripe-sync-worker');
|
|
3970
|
+
END IF;
|
|
3971
|
+
END $$;
|
|
3949
3972
|
`);
|
|
3950
3973
|
} catch (err) {
|
|
3951
3974
|
console.warn("Could not unschedule pg_cron job:", err);
|
|
@@ -3961,15 +3984,31 @@ var SupabaseSetupClient = class {
|
|
|
3961
3984
|
try {
|
|
3962
3985
|
await this.runSQL(`
|
|
3963
3986
|
SELECT pg_terminate_backend(pid)
|
|
3964
|
-
FROM
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3987
|
+
FROM pg_locks l
|
|
3988
|
+
JOIN pg_class c ON l.relation = c.oid
|
|
3989
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
3990
|
+
WHERE n.nspname = 'stripe'
|
|
3991
|
+
AND l.pid != pg_backend_pid()
|
|
3968
3992
|
`);
|
|
3969
3993
|
} catch (err) {
|
|
3970
3994
|
console.warn("Could not terminate connections:", err);
|
|
3971
3995
|
}
|
|
3972
|
-
|
|
3996
|
+
let dropAttempts = 0;
|
|
3997
|
+
const maxAttempts = 3;
|
|
3998
|
+
while (dropAttempts < maxAttempts) {
|
|
3999
|
+
try {
|
|
4000
|
+
await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
|
|
4001
|
+
break;
|
|
4002
|
+
} catch (err) {
|
|
4003
|
+
dropAttempts++;
|
|
4004
|
+
if (dropAttempts >= maxAttempts) {
|
|
4005
|
+
throw new Error(
|
|
4006
|
+
`Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
4007
|
+
);
|
|
4008
|
+
}
|
|
4009
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
3973
4012
|
} catch (error) {
|
|
3974
4013
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3975
4014
|
}
|
|
@@ -3986,7 +4025,7 @@ var SupabaseSetupClient = class {
|
|
|
3986
4025
|
`from 'npm:stripe-experiment-sync@${version}'`
|
|
3987
4026
|
);
|
|
3988
4027
|
}
|
|
3989
|
-
async install(stripeKey, packageVersion) {
|
|
4028
|
+
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
|
3990
4029
|
const trimmedStripeKey = stripeKey.trim();
|
|
3991
4030
|
if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
3992
4031
|
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
@@ -4010,7 +4049,7 @@ var SupabaseSetupClient = class {
|
|
|
4010
4049
|
if (!setupResult.success) {
|
|
4011
4050
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
4012
4051
|
}
|
|
4013
|
-
await this.setupPgCronJob();
|
|
4052
|
+
await this.setupPgCronJob(workerIntervalSeconds);
|
|
4014
4053
|
await this.updateInstallationComment(
|
|
4015
4054
|
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
|
|
4016
4055
|
);
|
|
@@ -4023,14 +4062,20 @@ var SupabaseSetupClient = class {
|
|
|
4023
4062
|
}
|
|
4024
4063
|
};
|
|
4025
4064
|
async function install(params) {
|
|
4026
|
-
const {
|
|
4065
|
+
const {
|
|
4066
|
+
supabaseAccessToken,
|
|
4067
|
+
supabaseProjectRef,
|
|
4068
|
+
stripeKey,
|
|
4069
|
+
packageVersion,
|
|
4070
|
+
workerIntervalSeconds
|
|
4071
|
+
} = params;
|
|
4027
4072
|
const client = new SupabaseSetupClient({
|
|
4028
4073
|
accessToken: supabaseAccessToken,
|
|
4029
4074
|
projectRef: supabaseProjectRef,
|
|
4030
4075
|
projectBaseUrl: params.baseProjectUrl,
|
|
4031
4076
|
managementApiBaseUrl: params.baseManagementApiUrl
|
|
4032
4077
|
});
|
|
4033
|
-
await client.install(stripeKey, packageVersion);
|
|
4078
|
+
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
4034
4079
|
}
|
|
4035
4080
|
async function uninstall(params) {
|
|
4036
4081
|
const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
|
|
@@ -4481,7 +4526,8 @@ async function installCommand(options) {
|
|
|
4481
4526
|
supabaseAccessToken: accessToken,
|
|
4482
4527
|
supabaseProjectRef: projectRef,
|
|
4483
4528
|
stripeKey,
|
|
4484
|
-
packageVersion: options.packageVersion
|
|
4529
|
+
packageVersion: options.packageVersion,
|
|
4530
|
+
workerIntervalSeconds: options.workerInterval
|
|
4485
4531
|
});
|
|
4486
4532
|
console.log(import_chalk3.default.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
4487
4533
|
console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
|
package/dist/cli/lib.d.cts
CHANGED
package/dist/cli/lib.d.ts
CHANGED
package/dist/cli/lib.js
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
migrateCommand,
|
|
7
7
|
syncCommand,
|
|
8
8
|
uninstallCommand
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import "../chunk-
|
|
11
|
-
import "../chunk-
|
|
12
|
-
import "../chunk-
|
|
9
|
+
} from "../chunk-4MOFTZ53.js";
|
|
10
|
+
import "../chunk-LFXGDVMC.js";
|
|
11
|
+
import "../chunk-EVNKMOYA.js";
|
|
12
|
+
import "../chunk-XTL72K7K.js";
|
|
13
13
|
export {
|
|
14
14
|
backfillCommand,
|
|
15
15
|
createTunnel,
|
package/dist/index.cjs
CHANGED
|
@@ -46,7 +46,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
46
46
|
// package.json
|
|
47
47
|
var package_default = {
|
|
48
48
|
name: "stripe-experiment-sync",
|
|
49
|
-
version: "1.0.
|
|
49
|
+
version: "1.0.11",
|
|
50
50
|
private: false,
|
|
51
51
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
52
52
|
type: "module",
|
package/dist/index.d.cts
CHANGED
|
@@ -334,6 +334,36 @@ interface ProcessNextParams extends SyncParams {
|
|
|
334
334
|
/** Who/what triggered this sync (for observability) */
|
|
335
335
|
triggeredBy?: string;
|
|
336
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Installation status of the stripe-sync package
|
|
339
|
+
*/
|
|
340
|
+
type InstallationStatus = 'not_installed' | 'installing' | 'installed' | 'error';
|
|
341
|
+
/**
|
|
342
|
+
* Sync status for a single account (from sync_runs view)
|
|
343
|
+
*/
|
|
344
|
+
interface StripeSyncAccountState {
|
|
345
|
+
account_id: string;
|
|
346
|
+
started_at: string;
|
|
347
|
+
closed_at: string | null;
|
|
348
|
+
status: 'pending' | 'running' | 'complete' | 'error';
|
|
349
|
+
error_message: string | null;
|
|
350
|
+
total_processed: number;
|
|
351
|
+
total_objects: number;
|
|
352
|
+
complete_count: number;
|
|
353
|
+
error_count: number;
|
|
354
|
+
running_count: number;
|
|
355
|
+
pending_count: number;
|
|
356
|
+
triggered_by: string;
|
|
357
|
+
max_concurrent: number;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Response schema for the sync status endpoint
|
|
361
|
+
*/
|
|
362
|
+
interface StripeSyncState {
|
|
363
|
+
package_version: string;
|
|
364
|
+
installation_status: InstallationStatus;
|
|
365
|
+
sync_status: StripeSyncAccountState[];
|
|
366
|
+
}
|
|
337
367
|
|
|
338
368
|
/**
|
|
339
369
|
* Identifies a specific sync run.
|
|
@@ -654,4 +684,4 @@ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): P
|
|
|
654
684
|
|
|
655
685
|
declare const VERSION: string;
|
|
656
686
|
|
|
657
|
-
export { type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncConfig, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
|
|
687
|
+
export { type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
|
package/dist/index.d.ts
CHANGED
|
@@ -334,6 +334,36 @@ interface ProcessNextParams extends SyncParams {
|
|
|
334
334
|
/** Who/what triggered this sync (for observability) */
|
|
335
335
|
triggeredBy?: string;
|
|
336
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Installation status of the stripe-sync package
|
|
339
|
+
*/
|
|
340
|
+
type InstallationStatus = 'not_installed' | 'installing' | 'installed' | 'error';
|
|
341
|
+
/**
|
|
342
|
+
* Sync status for a single account (from sync_runs view)
|
|
343
|
+
*/
|
|
344
|
+
interface StripeSyncAccountState {
|
|
345
|
+
account_id: string;
|
|
346
|
+
started_at: string;
|
|
347
|
+
closed_at: string | null;
|
|
348
|
+
status: 'pending' | 'running' | 'complete' | 'error';
|
|
349
|
+
error_message: string | null;
|
|
350
|
+
total_processed: number;
|
|
351
|
+
total_objects: number;
|
|
352
|
+
complete_count: number;
|
|
353
|
+
error_count: number;
|
|
354
|
+
running_count: number;
|
|
355
|
+
pending_count: number;
|
|
356
|
+
triggered_by: string;
|
|
357
|
+
max_concurrent: number;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Response schema for the sync status endpoint
|
|
361
|
+
*/
|
|
362
|
+
interface StripeSyncState {
|
|
363
|
+
package_version: string;
|
|
364
|
+
installation_status: InstallationStatus;
|
|
365
|
+
sync_status: StripeSyncAccountState[];
|
|
366
|
+
}
|
|
337
367
|
|
|
338
368
|
/**
|
|
339
369
|
* Identifies a specific sync run.
|
|
@@ -654,4 +684,4 @@ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): P
|
|
|
654
684
|
|
|
655
685
|
declare const VERSION: string;
|
|
656
686
|
|
|
657
|
-
export { type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncConfig, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
|
|
687
|
+
export { type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
|
package/dist/index.js
CHANGED
package/dist/supabase/index.cjs
CHANGED
|
@@ -47,14 +47,14 @@ module.exports = __toCommonJS(supabase_exports);
|
|
|
47
47
|
// src/supabase/supabase.ts
|
|
48
48
|
var import_supabase_management_js = require("supabase-management-js");
|
|
49
49
|
|
|
50
|
-
// raw-ts:/
|
|
51
|
-
var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method
|
|
50
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
51
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
52
52
|
|
|
53
|
-
// raw-ts:/
|
|
53
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
54
54
|
var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
55
55
|
|
|
56
|
-
// raw-ts:/
|
|
57
|
-
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron
|
|
56
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
57
|
+
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
58
58
|
|
|
59
59
|
// src/supabase/edge-function-code.ts
|
|
60
60
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -64,7 +64,7 @@ var workerFunctionCode = stripe_worker_default;
|
|
|
64
64
|
// package.json
|
|
65
65
|
var package_default = {
|
|
66
66
|
name: "stripe-experiment-sync",
|
|
67
|
-
version: "1.0.
|
|
67
|
+
version: "1.0.11",
|
|
68
68
|
private: false,
|
|
69
69
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
70
70
|
type: "module",
|
|
@@ -210,8 +210,29 @@ var SupabaseSetupClient = class {
|
|
|
210
210
|
}
|
|
211
211
|
/**
|
|
212
212
|
* Setup pg_cron job to invoke worker function
|
|
213
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
213
214
|
*/
|
|
214
|
-
async setupPgCronJob() {
|
|
215
|
+
async setupPgCronJob(intervalSeconds = 60) {
|
|
216
|
+
if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
|
|
217
|
+
throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
|
|
218
|
+
}
|
|
219
|
+
let schedule;
|
|
220
|
+
if (intervalSeconds < 60) {
|
|
221
|
+
schedule = `${intervalSeconds} seconds`;
|
|
222
|
+
} else if (intervalSeconds % 60 === 0) {
|
|
223
|
+
const minutes = intervalSeconds / 60;
|
|
224
|
+
if (minutes < 60) {
|
|
225
|
+
schedule = `*/${minutes} * * * *`;
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
215
236
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
216
237
|
const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
|
|
217
238
|
const sql = `
|
|
@@ -239,11 +260,11 @@ var SupabaseSetupClient = class {
|
|
|
239
260
|
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
|
|
240
261
|
);
|
|
241
262
|
|
|
242
|
-
-- Create job to invoke worker
|
|
263
|
+
-- Create job to invoke worker at configured interval
|
|
243
264
|
-- Worker reads from pgmq, enqueues objects if empty, and processes sync work
|
|
244
265
|
SELECT cron.schedule(
|
|
245
266
|
'stripe-sync-worker',
|
|
246
|
-
'
|
|
267
|
+
'${schedule}',
|
|
247
268
|
$$
|
|
248
269
|
SELECT net.http_post(
|
|
249
270
|
url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
|
|
@@ -427,10 +448,12 @@ var SupabaseSetupClient = class {
|
|
|
427
448
|
await this.deleteSecret("STRIPE_SECRET_KEY");
|
|
428
449
|
try {
|
|
429
450
|
await this.runSQL(`
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
|
|
433
|
-
|
|
451
|
+
DO $$
|
|
452
|
+
BEGIN
|
|
453
|
+
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
|
|
454
|
+
PERFORM cron.unschedule('stripe-sync-worker');
|
|
455
|
+
END IF;
|
|
456
|
+
END $$;
|
|
434
457
|
`);
|
|
435
458
|
} catch (err) {
|
|
436
459
|
console.warn("Could not unschedule pg_cron job:", err);
|
|
@@ -446,15 +469,31 @@ var SupabaseSetupClient = class {
|
|
|
446
469
|
try {
|
|
447
470
|
await this.runSQL(`
|
|
448
471
|
SELECT pg_terminate_backend(pid)
|
|
449
|
-
FROM
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
472
|
+
FROM pg_locks l
|
|
473
|
+
JOIN pg_class c ON l.relation = c.oid
|
|
474
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
475
|
+
WHERE n.nspname = 'stripe'
|
|
476
|
+
AND l.pid != pg_backend_pid()
|
|
453
477
|
`);
|
|
454
478
|
} catch (err) {
|
|
455
479
|
console.warn("Could not terminate connections:", err);
|
|
456
480
|
}
|
|
457
|
-
|
|
481
|
+
let dropAttempts = 0;
|
|
482
|
+
const maxAttempts = 3;
|
|
483
|
+
while (dropAttempts < maxAttempts) {
|
|
484
|
+
try {
|
|
485
|
+
await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
|
|
486
|
+
break;
|
|
487
|
+
} catch (err) {
|
|
488
|
+
dropAttempts++;
|
|
489
|
+
if (dropAttempts >= maxAttempts) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
458
497
|
} catch (error) {
|
|
459
498
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
460
499
|
}
|
|
@@ -471,7 +510,7 @@ var SupabaseSetupClient = class {
|
|
|
471
510
|
`from 'npm:stripe-experiment-sync@${version}'`
|
|
472
511
|
);
|
|
473
512
|
}
|
|
474
|
-
async install(stripeKey, packageVersion) {
|
|
513
|
+
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
|
475
514
|
const trimmedStripeKey = stripeKey.trim();
|
|
476
515
|
if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
477
516
|
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
@@ -495,7 +534,7 @@ var SupabaseSetupClient = class {
|
|
|
495
534
|
if (!setupResult.success) {
|
|
496
535
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
497
536
|
}
|
|
498
|
-
await this.setupPgCronJob();
|
|
537
|
+
await this.setupPgCronJob(workerIntervalSeconds);
|
|
499
538
|
await this.updateInstallationComment(
|
|
500
539
|
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
|
|
501
540
|
);
|
|
@@ -508,14 +547,20 @@ var SupabaseSetupClient = class {
|
|
|
508
547
|
}
|
|
509
548
|
};
|
|
510
549
|
async function install(params) {
|
|
511
|
-
const {
|
|
550
|
+
const {
|
|
551
|
+
supabaseAccessToken,
|
|
552
|
+
supabaseProjectRef,
|
|
553
|
+
stripeKey,
|
|
554
|
+
packageVersion,
|
|
555
|
+
workerIntervalSeconds
|
|
556
|
+
} = params;
|
|
512
557
|
const client = new SupabaseSetupClient({
|
|
513
558
|
accessToken: supabaseAccessToken,
|
|
514
559
|
projectRef: supabaseProjectRef,
|
|
515
560
|
projectBaseUrl: params.baseProjectUrl,
|
|
516
561
|
managementApiBaseUrl: params.baseManagementApiUrl
|
|
517
562
|
});
|
|
518
|
-
await client.install(stripeKey, packageVersion);
|
|
563
|
+
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
519
564
|
}
|
|
520
565
|
async function uninstall(params) {
|
|
521
566
|
const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
|
|
@@ -39,8 +39,9 @@ declare class SupabaseSetupClient {
|
|
|
39
39
|
runSQL(sql: string): Promise<unknown>;
|
|
40
40
|
/**
|
|
41
41
|
* Setup pg_cron job to invoke worker function
|
|
42
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
42
43
|
*/
|
|
43
|
-
setupPgCronJob(): Promise<void>;
|
|
44
|
+
setupPgCronJob(intervalSeconds?: number): Promise<void>;
|
|
44
45
|
/**
|
|
45
46
|
* Get the webhook URL for this project
|
|
46
47
|
*/
|
|
@@ -97,13 +98,14 @@ declare class SupabaseSetupClient {
|
|
|
97
98
|
* Inject package version into Edge Function code
|
|
98
99
|
*/
|
|
99
100
|
private injectPackageVersion;
|
|
100
|
-
install(stripeKey: string, packageVersion?: string): Promise<void>;
|
|
101
|
+
install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number): Promise<void>;
|
|
101
102
|
}
|
|
102
103
|
declare function install(params: {
|
|
103
104
|
supabaseAccessToken: string;
|
|
104
105
|
supabaseProjectRef: string;
|
|
105
106
|
stripeKey: string;
|
|
106
107
|
packageVersion?: string;
|
|
108
|
+
workerIntervalSeconds?: number;
|
|
107
109
|
baseProjectUrl?: string;
|
|
108
110
|
baseManagementApiUrl?: string;
|
|
109
111
|
}): Promise<void>;
|
package/dist/supabase/index.d.ts
CHANGED
|
@@ -39,8 +39,9 @@ declare class SupabaseSetupClient {
|
|
|
39
39
|
runSQL(sql: string): Promise<unknown>;
|
|
40
40
|
/**
|
|
41
41
|
* Setup pg_cron job to invoke worker function
|
|
42
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
42
43
|
*/
|
|
43
|
-
setupPgCronJob(): Promise<void>;
|
|
44
|
+
setupPgCronJob(intervalSeconds?: number): Promise<void>;
|
|
44
45
|
/**
|
|
45
46
|
* Get the webhook URL for this project
|
|
46
47
|
*/
|
|
@@ -97,13 +98,14 @@ declare class SupabaseSetupClient {
|
|
|
97
98
|
* Inject package version into Edge Function code
|
|
98
99
|
*/
|
|
99
100
|
private injectPackageVersion;
|
|
100
|
-
install(stripeKey: string, packageVersion?: string): Promise<void>;
|
|
101
|
+
install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number): Promise<void>;
|
|
101
102
|
}
|
|
102
103
|
declare function install(params: {
|
|
103
104
|
supabaseAccessToken: string;
|
|
104
105
|
supabaseProjectRef: string;
|
|
105
106
|
stripeKey: string;
|
|
106
107
|
packageVersion?: string;
|
|
108
|
+
workerIntervalSeconds?: number;
|
|
107
109
|
baseProjectUrl?: string;
|
|
108
110
|
baseManagementApiUrl?: string;
|
|
109
111
|
}): Promise<void>;
|
package/dist/supabase/index.js
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
uninstall,
|
|
10
10
|
webhookFunctionCode,
|
|
11
11
|
workerFunctionCode
|
|
12
|
-
} from "../chunk-
|
|
13
|
-
import "../chunk-
|
|
12
|
+
} from "../chunk-EVNKMOYA.js";
|
|
13
|
+
import "../chunk-XTL72K7K.js";
|
|
14
14
|
export {
|
|
15
15
|
INSTALLATION_ERROR_SUFFIX,
|
|
16
16
|
INSTALLATION_INSTALLED_SUFFIX,
|