stripe-experiment-sync 1.0.9-beta.1765909347 → 1.0.9
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-SI3VFP3M.js → chunk-OLVA37VZ.js} +5 -5
- package/dist/{chunk-2CYYWBTD.js → chunk-PQ2T7XTY.js} +27 -486
- package/dist/{chunk-DBJCCGXP.js → chunk-PWJLHHPY.js} +10 -28
- package/dist/{chunk-3W3CERIG.js → chunk-RR5BGG4F.js} +1 -3
- package/dist/cli/index.cjs +43 -521
- package/dist/cli/index.js +6 -7
- package/dist/cli/lib.cjs +41 -518
- package/dist/cli/lib.d.cts +0 -2
- package/dist/cli/lib.d.ts +0 -2
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +27 -490
- package/dist/index.d.cts +4 -107
- package/dist/index.d.ts +4 -107
- package/dist/index.js +2 -6
- package/dist/supabase/index.cjs +5 -7
- package/dist/supabase/index.js +2 -2
- package/package.json +1 -3
- package/dist/migrations/0059_sigma_subscription_item_change_events_v2_beta.sql +0 -61
- package/dist/migrations/0060_sigma_exchange_rates_from_usd.sql +0 -38
package/dist/cli/lib.cjs
CHANGED
|
@@ -59,7 +59,6 @@ async function loadConfig(options) {
|
|
|
59
59
|
config.stripeApiKey = options.stripeKey || process.env.STRIPE_API_KEY || "";
|
|
60
60
|
config.ngrokAuthToken = options.ngrokToken || process.env.NGROK_AUTH_TOKEN || "";
|
|
61
61
|
config.databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
|
|
62
|
-
config.enableSigmaSync = options.enableSigmaSync ?? (process.env.ENABLE_SIGMA_SYNC !== void 0 ? process.env.ENABLE_SIGMA_SYNC === "true" : void 0);
|
|
63
62
|
const questions = [];
|
|
64
63
|
if (!config.stripeApiKey) {
|
|
65
64
|
questions.push({
|
|
@@ -71,8 +70,8 @@ async function loadConfig(options) {
|
|
|
71
70
|
if (!input || input.trim() === "") {
|
|
72
71
|
return "Stripe API key is required";
|
|
73
72
|
}
|
|
74
|
-
if (!input.startsWith("sk_")
|
|
75
|
-
return 'Stripe API key should start with "sk_"
|
|
73
|
+
if (!input.startsWith("sk_")) {
|
|
74
|
+
return 'Stripe API key should start with "sk_"';
|
|
76
75
|
}
|
|
77
76
|
return true;
|
|
78
77
|
}
|
|
@@ -95,29 +94,18 @@ async function loadConfig(options) {
|
|
|
95
94
|
}
|
|
96
95
|
});
|
|
97
96
|
}
|
|
98
|
-
if (config.enableSigmaSync === void 0) {
|
|
99
|
-
questions.push({
|
|
100
|
-
type: "confirm",
|
|
101
|
-
name: "enableSigmaSync",
|
|
102
|
-
message: "Enable Sigma sync? (Requires Sigma access in Stripe API key)",
|
|
103
|
-
default: false
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
97
|
if (questions.length > 0) {
|
|
107
|
-
console.log(import_chalk.default.yellow("\nMissing configuration. Please provide:"));
|
|
98
|
+
console.log(import_chalk.default.yellow("\nMissing required configuration. Please provide:"));
|
|
108
99
|
const answers = await import_inquirer.default.prompt(questions);
|
|
109
100
|
Object.assign(config, answers);
|
|
110
101
|
}
|
|
111
|
-
if (config.enableSigmaSync === void 0) {
|
|
112
|
-
config.enableSigmaSync = false;
|
|
113
|
-
}
|
|
114
102
|
return config;
|
|
115
103
|
}
|
|
116
104
|
|
|
117
105
|
// package.json
|
|
118
106
|
var package_default = {
|
|
119
107
|
name: "stripe-experiment-sync",
|
|
120
|
-
version: "1.0.9
|
|
108
|
+
version: "1.0.9",
|
|
121
109
|
private: false,
|
|
122
110
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
123
111
|
type: "module",
|
|
@@ -157,7 +145,6 @@ var package_default = {
|
|
|
157
145
|
dotenv: "^16.4.7",
|
|
158
146
|
express: "^4.18.2",
|
|
159
147
|
inquirer: "^12.3.0",
|
|
160
|
-
papaparse: "5.4.1",
|
|
161
148
|
pg: "^8.16.3",
|
|
162
149
|
"pg-node-migrations": "0.0.8",
|
|
163
150
|
stripe: "^17.7.0",
|
|
@@ -169,7 +156,6 @@ var package_default = {
|
|
|
169
156
|
"@types/express": "^4.17.21",
|
|
170
157
|
"@types/inquirer": "^9.0.7",
|
|
171
158
|
"@types/node": "^24.10.1",
|
|
172
|
-
"@types/papaparse": "5.3.16",
|
|
173
159
|
"@types/pg": "^8.15.5",
|
|
174
160
|
"@types/ws": "^8.5.13",
|
|
175
161
|
"@types/yesql": "^4.1.4",
|
|
@@ -199,60 +185,14 @@ var package_default = {
|
|
|
199
185
|
};
|
|
200
186
|
|
|
201
187
|
// src/stripeSync.ts
|
|
202
|
-
var
|
|
188
|
+
var import_stripe2 = __toESM(require("stripe"), 1);
|
|
203
189
|
var import_yesql2 = require("yesql");
|
|
204
190
|
|
|
205
191
|
// src/database/postgres.ts
|
|
206
192
|
var import_pg = __toESM(require("pg"), 1);
|
|
207
193
|
var import_yesql = require("yesql");
|
|
208
|
-
|
|
209
|
-
// src/database/QueryUtils.ts
|
|
210
|
-
var QueryUtils = class _QueryUtils {
|
|
211
|
-
constructor() {
|
|
212
|
-
}
|
|
213
|
-
static quoteIdent(name) {
|
|
214
|
-
return `"${name}"`;
|
|
215
|
-
}
|
|
216
|
-
static quotedList(names) {
|
|
217
|
-
return names.map(_QueryUtils.quoteIdent).join(", ");
|
|
218
|
-
}
|
|
219
|
-
static buildInsertParts(columns) {
|
|
220
|
-
const columnsSql = columns.map((c) => _QueryUtils.quoteIdent(c.column)).join(", ");
|
|
221
|
-
const valuesSql = columns.map((c, i) => {
|
|
222
|
-
const placeholder = `$${i + 1}`;
|
|
223
|
-
return `${placeholder}::${c.pgType}`;
|
|
224
|
-
}).join(", ");
|
|
225
|
-
const params = columns.map((c) => c.value);
|
|
226
|
-
return { columnsSql, valuesSql, params };
|
|
227
|
-
}
|
|
228
|
-
static buildRawJsonUpsertQuery(schema, table, columns, conflictTarget) {
|
|
229
|
-
const { columnsSql, valuesSql, params } = _QueryUtils.buildInsertParts(columns);
|
|
230
|
-
const conflictSql = _QueryUtils.quotedList(conflictTarget);
|
|
231
|
-
const tsParamIdx = columns.findIndex((c) => c.column === "_last_synced_at") + 1;
|
|
232
|
-
if (tsParamIdx <= 0) {
|
|
233
|
-
throw new Error("buildRawJsonUpsertQuery requires _last_synced_at column");
|
|
234
|
-
}
|
|
235
|
-
const sql3 = `
|
|
236
|
-
INSERT INTO ${_QueryUtils.quoteIdent(schema)}.${_QueryUtils.quoteIdent(table)} (${columnsSql})
|
|
237
|
-
VALUES (${valuesSql})
|
|
238
|
-
ON CONFLICT (${conflictSql})
|
|
239
|
-
DO UPDATE SET
|
|
240
|
-
"_raw_data" = EXCLUDED."_raw_data",
|
|
241
|
-
"_last_synced_at" = $${tsParamIdx},
|
|
242
|
-
"_account_id" = EXCLUDED."_account_id"
|
|
243
|
-
WHERE ${_QueryUtils.quoteIdent(table)}."_last_synced_at" IS NULL
|
|
244
|
-
OR ${_QueryUtils.quoteIdent(table)}."_last_synced_at" < $${tsParamIdx}
|
|
245
|
-
RETURNING *
|
|
246
|
-
`;
|
|
247
|
-
return { sql: sql3, params };
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// src/database/postgres.ts
|
|
252
194
|
var ORDERED_STRIPE_TABLES = [
|
|
253
|
-
"exchange_rates_from_usd",
|
|
254
195
|
"subscription_items",
|
|
255
|
-
"subscription_item_change_events_v2_beta",
|
|
256
196
|
"subscriptions",
|
|
257
197
|
"subscription_schedules",
|
|
258
198
|
"checkout_session_line_items",
|
|
@@ -322,7 +262,7 @@ var PostgresClient = class {
|
|
|
322
262
|
}
|
|
323
263
|
return results.flatMap((it) => it.rows);
|
|
324
264
|
}
|
|
325
|
-
async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp
|
|
265
|
+
async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp) {
|
|
326
266
|
const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
327
267
|
if (!entries.length) return [];
|
|
328
268
|
const chunkSize = 5;
|
|
@@ -357,33 +297,20 @@ var PostgresClient = class {
|
|
|
357
297
|
const prepared = (0, import_yesql.pg)(upsertSql, { useNullForMissing: true })(cleansed);
|
|
358
298
|
queries.push(this.pool.query(prepared.text, prepared.values));
|
|
359
299
|
} else {
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
];
|
|
375
|
-
for (const c of columns) {
|
|
376
|
-
if (c.value === void 0) {
|
|
377
|
-
throw new Error(`Missing required value for ${table}.${c.column}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
const { sql: upsertSql, params } = QueryUtils.buildRawJsonUpsertQuery(
|
|
381
|
-
this.config.schema,
|
|
382
|
-
table,
|
|
383
|
-
columns,
|
|
384
|
-
conflictTarget
|
|
385
|
-
);
|
|
386
|
-
queries.push(this.pool.query(upsertSql, params));
|
|
300
|
+
const rawData = JSON.stringify(entry);
|
|
301
|
+
const upsertSql = `
|
|
302
|
+
INSERT INTO "${this.config.schema}"."${table}" ("_raw_data", "_last_synced_at", "_account_id")
|
|
303
|
+
VALUES ($1::jsonb, $2, $3)
|
|
304
|
+
ON CONFLICT (id)
|
|
305
|
+
DO UPDATE SET
|
|
306
|
+
"_raw_data" = EXCLUDED."_raw_data",
|
|
307
|
+
"_last_synced_at" = $2,
|
|
308
|
+
"_account_id" = EXCLUDED."_account_id"
|
|
309
|
+
WHERE "${table}"."_last_synced_at" IS NULL
|
|
310
|
+
OR "${table}"."_last_synced_at" < $2
|
|
311
|
+
RETURNING *
|
|
312
|
+
`;
|
|
313
|
+
queries.push(this.pool.query(upsertSql, [rawData, timestamp, accountId]));
|
|
387
314
|
}
|
|
388
315
|
});
|
|
389
316
|
results.push(...await Promise.all(queries));
|
|
@@ -783,12 +710,7 @@ var PostgresClient = class {
|
|
|
783
710
|
} else {
|
|
784
711
|
await this.query(
|
|
785
712
|
`UPDATE "${this.config.schema}"."_sync_obj_runs"
|
|
786
|
-
SET cursor =
|
|
787
|
-
WHEN cursor IS NULL THEN $4
|
|
788
|
-
WHEN (cursor COLLATE "C") < ($4::text COLLATE "C") THEN $4
|
|
789
|
-
ELSE cursor
|
|
790
|
-
END,
|
|
791
|
-
updated_at = now()
|
|
713
|
+
SET cursor = $4, updated_at = now()
|
|
792
714
|
WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
|
|
793
715
|
[accountId, runStartedAt, object, cursor]
|
|
794
716
|
);
|
|
@@ -799,17 +721,10 @@ var PostgresClient = class {
|
|
|
799
721
|
* This considers completed, error, AND running runs to ensure recovery syncs
|
|
800
722
|
* don't re-process data that was already synced before a crash.
|
|
801
723
|
* A 'running' status with a cursor means the process was killed mid-sync.
|
|
802
|
-
*
|
|
803
|
-
* Handles two cursor formats:
|
|
804
|
-
* - Numeric: compared as bigint for correct ordering
|
|
805
|
-
* - Composite cursors: compared as strings with COLLATE "C"
|
|
806
724
|
*/
|
|
807
725
|
async getLastCompletedCursor(accountId, object) {
|
|
808
726
|
const result = await this.query(
|
|
809
|
-
`SELECT
|
|
810
|
-
WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
|
|
811
|
-
ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
|
|
812
|
-
END as cursor
|
|
727
|
+
`SELECT MAX(o.cursor::bigint)::text as cursor
|
|
813
728
|
FROM "${this.config.schema}"."_sync_obj_runs" o
|
|
814
729
|
WHERE o."_account_id" = $1
|
|
815
730
|
AND o.object = $2
|
|
@@ -1094,269 +1009,6 @@ function hashApiKey(apiKey) {
|
|
|
1094
1009
|
return (0, import_crypto.createHash)("sha256").update(apiKey).digest("hex");
|
|
1095
1010
|
}
|
|
1096
1011
|
|
|
1097
|
-
// src/sigma/sigmaApi.ts
|
|
1098
|
-
var import_papaparse = __toESM(require("papaparse"), 1);
|
|
1099
|
-
var import_stripe2 = __toESM(require("stripe"), 1);
|
|
1100
|
-
var STRIPE_FILES_BASE = "https://files.stripe.com/v1";
|
|
1101
|
-
function sleep2(ms) {
|
|
1102
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1103
|
-
}
|
|
1104
|
-
function parseCsvObjects(csv) {
|
|
1105
|
-
const input = csv.replace(/^\uFEFF/, "");
|
|
1106
|
-
const parsed = import_papaparse.default.parse(input, {
|
|
1107
|
-
header: true,
|
|
1108
|
-
skipEmptyLines: "greedy"
|
|
1109
|
-
});
|
|
1110
|
-
if (parsed.errors.length > 0) {
|
|
1111
|
-
throw new Error(`Failed to parse Sigma CSV: ${parsed.errors[0]?.message ?? "unknown error"}`);
|
|
1112
|
-
}
|
|
1113
|
-
return parsed.data.filter((row) => row && Object.keys(row).length > 0).map(
|
|
1114
|
-
(row) => Object.fromEntries(
|
|
1115
|
-
Object.entries(row).map(([k, v]) => [k, v == null || v === "" ? null : String(v)])
|
|
1116
|
-
)
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
function normalizeSigmaTimestampToIso(value) {
|
|
1120
|
-
const v = value.trim();
|
|
1121
|
-
if (!v) return null;
|
|
1122
|
-
const hasExplicitTz = /z$|[+-]\d{2}:?\d{2}$/i.test(v);
|
|
1123
|
-
const isoish = v.includes("T") ? v : v.replace(" ", "T");
|
|
1124
|
-
const candidate = hasExplicitTz ? isoish : `${isoish}Z`;
|
|
1125
|
-
const d = new Date(candidate);
|
|
1126
|
-
if (Number.isNaN(d.getTime())) return null;
|
|
1127
|
-
return d.toISOString();
|
|
1128
|
-
}
|
|
1129
|
-
async function fetchStripeText(url, apiKey, options) {
|
|
1130
|
-
const res = await fetch(url, {
|
|
1131
|
-
...options,
|
|
1132
|
-
headers: {
|
|
1133
|
-
...options.headers ?? {},
|
|
1134
|
-
Authorization: `Bearer ${apiKey}`
|
|
1135
|
-
}
|
|
1136
|
-
});
|
|
1137
|
-
const text = await res.text();
|
|
1138
|
-
if (!res.ok) {
|
|
1139
|
-
throw new Error(`Sigma file download error (${res.status}) for ${url}: ${text}`);
|
|
1140
|
-
}
|
|
1141
|
-
return text;
|
|
1142
|
-
}
|
|
1143
|
-
async function runSigmaQueryAndDownloadCsv(params) {
|
|
1144
|
-
const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
|
|
1145
|
-
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
1146
|
-
const stripe = new import_stripe2.default(params.apiKey);
|
|
1147
|
-
const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
|
|
1148
|
-
sql: params.sql
|
|
1149
|
-
});
|
|
1150
|
-
const queryRunId = created.id;
|
|
1151
|
-
const start = Date.now();
|
|
1152
|
-
let current = created;
|
|
1153
|
-
while (current.status === "running") {
|
|
1154
|
-
if (Date.now() - start > pollTimeoutMs) {
|
|
1155
|
-
throw new Error(`Sigma query run timed out after ${pollTimeoutMs}ms: ${queryRunId}`);
|
|
1156
|
-
}
|
|
1157
|
-
await sleep2(pollIntervalMs);
|
|
1158
|
-
current = await stripe.rawRequest(
|
|
1159
|
-
"GET",
|
|
1160
|
-
`/v1/sigma/query_runs/${queryRunId}`,
|
|
1161
|
-
{}
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
if (current.status !== "succeeded") {
|
|
1165
|
-
throw new Error(
|
|
1166
|
-
`Sigma query run did not succeed (status=${current.status}) id=${queryRunId} error=${JSON.stringify(
|
|
1167
|
-
current.error
|
|
1168
|
-
)}`
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
const fileId = current.result?.file;
|
|
1172
|
-
if (!fileId) {
|
|
1173
|
-
throw new Error(`Sigma query run succeeded but result.file is missing (id=${queryRunId})`);
|
|
1174
|
-
}
|
|
1175
|
-
const csv = await fetchStripeText(
|
|
1176
|
-
`${STRIPE_FILES_BASE}/files/${fileId}/contents`,
|
|
1177
|
-
params.apiKey,
|
|
1178
|
-
{ method: "GET" }
|
|
1179
|
-
);
|
|
1180
|
-
return { queryRunId, fileId, csv };
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// src/sigma/sigmaIngestionConfigs.ts
|
|
1184
|
-
var SIGMA_INGESTION_CONFIGS = {
|
|
1185
|
-
subscription_item_change_events_v2_beta: {
|
|
1186
|
-
sigmaTable: "subscription_item_change_events_v2_beta",
|
|
1187
|
-
destinationTable: "subscription_item_change_events_v2_beta",
|
|
1188
|
-
pageSize: 1e4,
|
|
1189
|
-
cursor: {
|
|
1190
|
-
version: 1,
|
|
1191
|
-
columns: [
|
|
1192
|
-
{ column: "event_timestamp", type: "timestamp" },
|
|
1193
|
-
{ column: "event_type", type: "string" },
|
|
1194
|
-
{ column: "subscription_item_id", type: "string" }
|
|
1195
|
-
]
|
|
1196
|
-
},
|
|
1197
|
-
upsert: {
|
|
1198
|
-
conflictTarget: ["_account_id", "event_timestamp", "event_type", "subscription_item_id"],
|
|
1199
|
-
extraColumns: [
|
|
1200
|
-
{ column: "event_timestamp", pgType: "timestamptz", entryKey: "event_timestamp" },
|
|
1201
|
-
{ column: "event_type", pgType: "text", entryKey: "event_type" },
|
|
1202
|
-
{ column: "subscription_item_id", pgType: "text", entryKey: "subscription_item_id" }
|
|
1203
|
-
]
|
|
1204
|
-
}
|
|
1205
|
-
},
|
|
1206
|
-
exchange_rates_from_usd: {
|
|
1207
|
-
sigmaTable: "exchange_rates_from_usd",
|
|
1208
|
-
destinationTable: "exchange_rates_from_usd",
|
|
1209
|
-
pageSize: 1e4,
|
|
1210
|
-
cursor: {
|
|
1211
|
-
version: 1,
|
|
1212
|
-
columns: [
|
|
1213
|
-
{ column: "date", type: "string" },
|
|
1214
|
-
{ column: "sell_currency", type: "string" }
|
|
1215
|
-
]
|
|
1216
|
-
},
|
|
1217
|
-
upsert: {
|
|
1218
|
-
conflictTarget: ["_account_id", "date", "sell_currency"],
|
|
1219
|
-
extraColumns: [
|
|
1220
|
-
{ column: "date", pgType: "date", entryKey: "date" },
|
|
1221
|
-
{ column: "sell_currency", pgType: "text", entryKey: "sell_currency" }
|
|
1222
|
-
]
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
// src/sigma/sigmaIngestion.ts
|
|
1228
|
-
var SIGMA_CURSOR_DELIM = "";
|
|
1229
|
-
function escapeSigmaSqlStringLiteral(value) {
|
|
1230
|
-
return value.replace(/'/g, "''");
|
|
1231
|
-
}
|
|
1232
|
-
function formatSigmaTimestampForSqlLiteral(date) {
|
|
1233
|
-
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
1234
|
-
}
|
|
1235
|
-
function decodeSigmaCursorValues(spec, cursor) {
|
|
1236
|
-
const prefix = `v${spec.version}${SIGMA_CURSOR_DELIM}`;
|
|
1237
|
-
if (!cursor.startsWith(prefix)) {
|
|
1238
|
-
throw new Error(
|
|
1239
|
-
`Unrecognized Sigma cursor format (expected prefix ${JSON.stringify(prefix)}): ${cursor}`
|
|
1240
|
-
);
|
|
1241
|
-
}
|
|
1242
|
-
const parts = cursor.split(SIGMA_CURSOR_DELIM);
|
|
1243
|
-
const expected = 1 + spec.columns.length;
|
|
1244
|
-
if (parts.length !== expected) {
|
|
1245
|
-
throw new Error(`Malformed Sigma cursor: expected ${expected} parts, got ${parts.length}`);
|
|
1246
|
-
}
|
|
1247
|
-
return parts.slice(1);
|
|
1248
|
-
}
|
|
1249
|
-
function encodeSigmaCursor(spec, values) {
|
|
1250
|
-
if (values.length !== spec.columns.length) {
|
|
1251
|
-
throw new Error(
|
|
1252
|
-
`Cannot encode Sigma cursor: expected ${spec.columns.length} values, got ${values.length}`
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
for (const v of values) {
|
|
1256
|
-
if (v.includes(SIGMA_CURSOR_DELIM)) {
|
|
1257
|
-
throw new Error("Cannot encode Sigma cursor: value contains delimiter character");
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
return [`v${spec.version}`, ...values].join(SIGMA_CURSOR_DELIM);
|
|
1261
|
-
}
|
|
1262
|
-
function sigmaSqlLiteralForCursorValue(spec, rawValue) {
|
|
1263
|
-
switch (spec.type) {
|
|
1264
|
-
case "timestamp": {
|
|
1265
|
-
const d = new Date(rawValue);
|
|
1266
|
-
if (Number.isNaN(d.getTime())) {
|
|
1267
|
-
throw new Error(`Invalid timestamp cursor value for ${spec.column}: ${rawValue}`);
|
|
1268
|
-
}
|
|
1269
|
-
return `timestamp '${formatSigmaTimestampForSqlLiteral(d)}'`;
|
|
1270
|
-
}
|
|
1271
|
-
case "number": {
|
|
1272
|
-
if (!/^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
1273
|
-
throw new Error(`Invalid numeric cursor value for ${spec.column}: ${rawValue}`);
|
|
1274
|
-
}
|
|
1275
|
-
return rawValue;
|
|
1276
|
-
}
|
|
1277
|
-
case "string":
|
|
1278
|
-
return `'${escapeSigmaSqlStringLiteral(rawValue)}'`;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
function buildSigmaCursorWhereClause(spec, cursorValues) {
|
|
1282
|
-
if (cursorValues.length !== spec.columns.length) {
|
|
1283
|
-
throw new Error(
|
|
1284
|
-
`Cannot build Sigma cursor predicate: expected ${spec.columns.length} values, got ${cursorValues.length}`
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1287
|
-
const cols = spec.columns.map((c) => c.column);
|
|
1288
|
-
const lits = spec.columns.map((c, i) => sigmaSqlLiteralForCursorValue(c, cursorValues[i] ?? ""));
|
|
1289
|
-
const ors = [];
|
|
1290
|
-
for (let i = 0; i < cols.length; i++) {
|
|
1291
|
-
const ands = [];
|
|
1292
|
-
for (let j = 0; j < i; j++) {
|
|
1293
|
-
ands.push(`${cols[j]} = ${lits[j]}`);
|
|
1294
|
-
}
|
|
1295
|
-
ands.push(`${cols[i]} > ${lits[i]}`);
|
|
1296
|
-
ors.push(`(${ands.join(" AND ")})`);
|
|
1297
|
-
}
|
|
1298
|
-
return ors.join(" OR ");
|
|
1299
|
-
}
|
|
1300
|
-
function buildSigmaQuery(config, cursor) {
|
|
1301
|
-
const select = config.select === void 0 || config.select === "*" ? "*" : config.select.join(", ");
|
|
1302
|
-
const whereParts = [];
|
|
1303
|
-
if (config.additionalWhere) {
|
|
1304
|
-
whereParts.push(`(${config.additionalWhere})`);
|
|
1305
|
-
}
|
|
1306
|
-
if (cursor) {
|
|
1307
|
-
const values = decodeSigmaCursorValues(config.cursor, cursor);
|
|
1308
|
-
const predicate = buildSigmaCursorWhereClause(config.cursor, values);
|
|
1309
|
-
whereParts.push(`(${predicate})`);
|
|
1310
|
-
}
|
|
1311
|
-
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : "";
|
|
1312
|
-
const orderBy = config.cursor.columns.map((c) => c.column).join(", ");
|
|
1313
|
-
return [
|
|
1314
|
-
`SELECT ${select} FROM ${config.sigmaTable}`,
|
|
1315
|
-
whereClause,
|
|
1316
|
-
`ORDER BY ${orderBy} ASC`,
|
|
1317
|
-
`LIMIT ${config.pageSize}`
|
|
1318
|
-
].filter(Boolean).join(" ");
|
|
1319
|
-
}
|
|
1320
|
-
function defaultSigmaRowToEntry(config, row) {
|
|
1321
|
-
const out = { ...row };
|
|
1322
|
-
for (const col of config.cursor.columns) {
|
|
1323
|
-
const raw = row[col.column];
|
|
1324
|
-
if (raw == null) {
|
|
1325
|
-
throw new Error(`Sigma row missing required cursor column: ${col.column}`);
|
|
1326
|
-
}
|
|
1327
|
-
if (col.type === "timestamp") {
|
|
1328
|
-
const normalized = normalizeSigmaTimestampToIso(raw);
|
|
1329
|
-
if (!normalized) {
|
|
1330
|
-
throw new Error(`Sigma row has invalid timestamp for ${col.column}: ${raw}`);
|
|
1331
|
-
}
|
|
1332
|
-
out[col.column] = normalized;
|
|
1333
|
-
} else if (col.type === "string") {
|
|
1334
|
-
const v = raw.trim();
|
|
1335
|
-
if (!v) {
|
|
1336
|
-
throw new Error(`Sigma row has empty string for required cursor column: ${col.column}`);
|
|
1337
|
-
}
|
|
1338
|
-
out[col.column] = v;
|
|
1339
|
-
} else {
|
|
1340
|
-
const v = raw.trim();
|
|
1341
|
-
if (!v) {
|
|
1342
|
-
throw new Error(`Sigma row has empty value for required cursor column: ${col.column}`);
|
|
1343
|
-
}
|
|
1344
|
-
out[col.column] = v;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
return out;
|
|
1348
|
-
}
|
|
1349
|
-
function sigmaCursorFromEntry(config, entry) {
|
|
1350
|
-
const values = config.cursor.columns.map((c) => {
|
|
1351
|
-
const raw = entry[c.column];
|
|
1352
|
-
if (raw == null) {
|
|
1353
|
-
throw new Error(`Cannot build cursor: entry missing ${c.column}`);
|
|
1354
|
-
}
|
|
1355
|
-
return String(raw);
|
|
1356
|
-
});
|
|
1357
|
-
return encodeSigmaCursor(config.cursor, values);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
1012
|
// src/stripeSync.ts
|
|
1361
1013
|
function getUniqueIds(entries, key) {
|
|
1362
1014
|
const set = new Set(
|
|
@@ -1367,7 +1019,7 @@ function getUniqueIds(entries, key) {
|
|
|
1367
1019
|
var StripeSync = class {
|
|
1368
1020
|
constructor(config) {
|
|
1369
1021
|
this.config = config;
|
|
1370
|
-
const baseStripe = new
|
|
1022
|
+
const baseStripe = new import_stripe2.default(config.stripeSecretKey, {
|
|
1371
1023
|
// https://github.com/stripe/stripe-node#configuration
|
|
1372
1024
|
// @ts-ignore
|
|
1373
1025
|
apiVersion: config.stripeApiVersion,
|
|
@@ -1768,17 +1420,6 @@ var StripeSync = class {
|
|
|
1768
1420
|
listFn: (p) => this.stripe.checkout.sessions.list(p),
|
|
1769
1421
|
upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
|
|
1770
1422
|
supportsCreatedFilter: true
|
|
1771
|
-
},
|
|
1772
|
-
// Sigma-backed resources
|
|
1773
|
-
subscription_item_change_events_v2_beta: {
|
|
1774
|
-
order: 18,
|
|
1775
|
-
supportsCreatedFilter: false,
|
|
1776
|
-
sigma: SIGMA_INGESTION_CONFIGS.subscription_item_change_events_v2_beta
|
|
1777
|
-
},
|
|
1778
|
-
exchange_rates_from_usd: {
|
|
1779
|
-
order: 19,
|
|
1780
|
-
supportsCreatedFilter: false,
|
|
1781
|
-
sigma: SIGMA_INGESTION_CONFIGS.exchange_rates_from_usd
|
|
1782
1423
|
}
|
|
1783
1424
|
};
|
|
1784
1425
|
async processEvent(event) {
|
|
@@ -1811,13 +1452,7 @@ var StripeSync = class {
|
|
|
1811
1452
|
* Order is determined by the `order` field in resourceRegistry.
|
|
1812
1453
|
*/
|
|
1813
1454
|
getSupportedSyncObjects() {
|
|
1814
|
-
|
|
1815
|
-
if (!this.config.enableSigmaSync) {
|
|
1816
|
-
return all.filter(
|
|
1817
|
-
(o) => o !== "subscription_item_change_events_v2_beta" && o !== "exchange_rates_from_usd"
|
|
1818
|
-
);
|
|
1819
|
-
}
|
|
1820
|
-
return all;
|
|
1455
|
+
return Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
|
|
1821
1456
|
}
|
|
1822
1457
|
// Event handler methods
|
|
1823
1458
|
async handleChargeEvent(event, accountId) {
|
|
@@ -1896,7 +1531,7 @@ var StripeSync = class {
|
|
|
1896
1531
|
);
|
|
1897
1532
|
await this.upsertProducts([product], accountId, this.getSyncTimestamp(event, refetched));
|
|
1898
1533
|
} catch (err) {
|
|
1899
|
-
if (err instanceof
|
|
1534
|
+
if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1900
1535
|
const product = event.data.object;
|
|
1901
1536
|
await this.deleteProduct(product.id);
|
|
1902
1537
|
} else {
|
|
@@ -1916,7 +1551,7 @@ var StripeSync = class {
|
|
|
1916
1551
|
);
|
|
1917
1552
|
await this.upsertPrices([price], accountId, false, this.getSyncTimestamp(event, refetched));
|
|
1918
1553
|
} catch (err) {
|
|
1919
|
-
if (err instanceof
|
|
1554
|
+
if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1920
1555
|
const price = event.data.object;
|
|
1921
1556
|
await this.deletePrice(price.id);
|
|
1922
1557
|
} else {
|
|
@@ -1936,7 +1571,7 @@ var StripeSync = class {
|
|
|
1936
1571
|
);
|
|
1937
1572
|
await this.upsertPlans([plan], accountId, false, this.getSyncTimestamp(event, refetched));
|
|
1938
1573
|
} catch (err) {
|
|
1939
|
-
if (err instanceof
|
|
1574
|
+
if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1940
1575
|
const plan = event.data.object;
|
|
1941
1576
|
await this.deletePlan(plan.id);
|
|
1942
1577
|
} else {
|
|
@@ -2183,10 +1818,10 @@ var StripeSync = class {
|
|
|
2183
1818
|
let cursor = null;
|
|
2184
1819
|
if (!params?.created) {
|
|
2185
1820
|
if (objRun?.cursor) {
|
|
2186
|
-
cursor = objRun.cursor;
|
|
1821
|
+
cursor = parseInt(objRun.cursor);
|
|
2187
1822
|
} else {
|
|
2188
1823
|
const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
|
|
2189
|
-
cursor = lastCursor
|
|
1824
|
+
cursor = lastCursor ? parseInt(lastCursor) : null;
|
|
2190
1825
|
}
|
|
2191
1826
|
}
|
|
2192
1827
|
const result = await this.fetchOnePage(
|
|
@@ -2241,18 +1876,9 @@ var StripeSync = class {
|
|
|
2241
1876
|
throw new Error(`Unsupported object type for processNext: ${object}`);
|
|
2242
1877
|
}
|
|
2243
1878
|
try {
|
|
2244
|
-
if (config.sigma) {
|
|
2245
|
-
return await this.fetchOneSigmaPage(
|
|
2246
|
-
accountId,
|
|
2247
|
-
resourceName,
|
|
2248
|
-
runStartedAt,
|
|
2249
|
-
cursor,
|
|
2250
|
-
config.sigma
|
|
2251
|
-
);
|
|
2252
|
-
}
|
|
2253
1879
|
const listParams = { limit };
|
|
2254
1880
|
if (config.supportsCreatedFilter) {
|
|
2255
|
-
const created = params?.created ?? (cursor
|
|
1881
|
+
const created = params?.created ?? (cursor ? { gte: cursor } : void 0);
|
|
2256
1882
|
if (created) {
|
|
2257
1883
|
listParams.created = created;
|
|
2258
1884
|
}
|
|
@@ -2297,97 +1923,6 @@ var StripeSync = class {
|
|
|
2297
1923
|
throw error;
|
|
2298
1924
|
}
|
|
2299
1925
|
}
|
|
2300
|
-
async getSigmaFallbackCursorFromDestination(accountId, sigmaConfig) {
|
|
2301
|
-
const cursorCols = sigmaConfig.cursor.columns;
|
|
2302
|
-
const selectCols = cursorCols.map((c) => `"${c.column}"`).join(", ");
|
|
2303
|
-
const orderBy = cursorCols.map((c) => `"${c.column}" DESC`).join(", ");
|
|
2304
|
-
const result = await this.postgresClient.query(
|
|
2305
|
-
`SELECT ${selectCols}
|
|
2306
|
-
FROM "stripe"."${sigmaConfig.destinationTable}"
|
|
2307
|
-
WHERE "_account_id" = $1
|
|
2308
|
-
ORDER BY ${orderBy}
|
|
2309
|
-
LIMIT 1`,
|
|
2310
|
-
[accountId]
|
|
2311
|
-
);
|
|
2312
|
-
if (result.rows.length === 0) return null;
|
|
2313
|
-
const row = result.rows[0];
|
|
2314
|
-
const entryForCursor = {};
|
|
2315
|
-
for (const c of cursorCols) {
|
|
2316
|
-
const v = row[c.column];
|
|
2317
|
-
if (v == null) {
|
|
2318
|
-
throw new Error(
|
|
2319
|
-
`Sigma fallback cursor query returned null for ${sigmaConfig.destinationTable}.${c.column}`
|
|
2320
|
-
);
|
|
2321
|
-
}
|
|
2322
|
-
if (c.type === "timestamp") {
|
|
2323
|
-
const d = v instanceof Date ? v : new Date(String(v));
|
|
2324
|
-
if (Number.isNaN(d.getTime())) {
|
|
2325
|
-
throw new Error(
|
|
2326
|
-
`Sigma fallback cursor query returned invalid timestamp for ${sigmaConfig.destinationTable}.${c.column}: ${String(
|
|
2327
|
-
v
|
|
2328
|
-
)}`
|
|
2329
|
-
);
|
|
2330
|
-
}
|
|
2331
|
-
entryForCursor[c.column] = d.toISOString();
|
|
2332
|
-
} else {
|
|
2333
|
-
entryForCursor[c.column] = String(v);
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
return sigmaCursorFromEntry(sigmaConfig, entryForCursor);
|
|
2337
|
-
}
|
|
2338
|
-
async fetchOneSigmaPage(accountId, resourceName, runStartedAt, cursor, sigmaConfig) {
|
|
2339
|
-
if (!this.config.stripeSecretKey) {
|
|
2340
|
-
throw new Error("Sigma sync requested but stripeSecretKey is not configured.");
|
|
2341
|
-
}
|
|
2342
|
-
if (resourceName !== sigmaConfig.destinationTable) {
|
|
2343
|
-
throw new Error(
|
|
2344
|
-
`Sigma sync config mismatch: resourceName=${resourceName} destinationTable=${sigmaConfig.destinationTable}`
|
|
2345
|
-
);
|
|
2346
|
-
}
|
|
2347
|
-
const effectiveCursor = cursor ?? await this.getSigmaFallbackCursorFromDestination(accountId, sigmaConfig);
|
|
2348
|
-
const sigmaSql = buildSigmaQuery(sigmaConfig, effectiveCursor);
|
|
2349
|
-
this.config.logger?.info(
|
|
2350
|
-
{ object: resourceName, pageSize: sigmaConfig.pageSize, hasCursor: Boolean(effectiveCursor) },
|
|
2351
|
-
"Sigma sync: running query"
|
|
2352
|
-
);
|
|
2353
|
-
const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
|
|
2354
|
-
apiKey: this.config.stripeSecretKey,
|
|
2355
|
-
sql: sigmaSql,
|
|
2356
|
-
logger: this.config.logger
|
|
2357
|
-
});
|
|
2358
|
-
const rows = parseCsvObjects(csv);
|
|
2359
|
-
if (rows.length === 0) {
|
|
2360
|
-
await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
|
|
2361
|
-
return { processed: 0, hasMore: false, runStartedAt };
|
|
2362
|
-
}
|
|
2363
|
-
const entries = rows.map(
|
|
2364
|
-
(row) => defaultSigmaRowToEntry(sigmaConfig, row)
|
|
2365
|
-
);
|
|
2366
|
-
this.config.logger?.info(
|
|
2367
|
-
{ object: resourceName, rows: entries.length, queryRunId, fileId },
|
|
2368
|
-
"Sigma sync: upserting rows"
|
|
2369
|
-
);
|
|
2370
|
-
await this.postgresClient.upsertManyWithTimestampProtection(
|
|
2371
|
-
entries,
|
|
2372
|
-
resourceName,
|
|
2373
|
-
accountId,
|
|
2374
|
-
void 0,
|
|
2375
|
-
sigmaConfig.upsert
|
|
2376
|
-
);
|
|
2377
|
-
await this.postgresClient.incrementObjectProgress(
|
|
2378
|
-
accountId,
|
|
2379
|
-
runStartedAt,
|
|
2380
|
-
resourceName,
|
|
2381
|
-
entries.length
|
|
2382
|
-
);
|
|
2383
|
-
const newCursor = sigmaCursorFromEntry(sigmaConfig, entries[entries.length - 1]);
|
|
2384
|
-
await this.postgresClient.updateObjectCursor(accountId, runStartedAt, resourceName, newCursor);
|
|
2385
|
-
const hasMore = rows.length === sigmaConfig.pageSize;
|
|
2386
|
-
if (!hasMore) {
|
|
2387
|
-
await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
|
|
2388
|
-
}
|
|
2389
|
-
return { processed: entries.length, hasMore, runStartedAt };
|
|
2390
|
-
}
|
|
2391
1926
|
/**
|
|
2392
1927
|
* Process all pages for all (or specified) object types until complete.
|
|
2393
1928
|
*
|
|
@@ -2516,12 +2051,6 @@ var StripeSync = class {
|
|
|
2516
2051
|
case "checkout_sessions":
|
|
2517
2052
|
results.checkoutSessions = result;
|
|
2518
2053
|
break;
|
|
2519
|
-
case "subscription_item_change_events_v2_beta":
|
|
2520
|
-
results.subscriptionItemChangeEventsV2Beta = result;
|
|
2521
|
-
break;
|
|
2522
|
-
case "exchange_rates_from_usd":
|
|
2523
|
-
results.exchangeRatesFromUsd = result;
|
|
2524
|
-
break;
|
|
2525
2054
|
}
|
|
2526
2055
|
}
|
|
2527
2056
|
}
|
|
@@ -4115,14 +3644,14 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
4115
3644
|
// src/supabase/supabase.ts
|
|
4116
3645
|
var import_supabase_management_js = require("supabase-management-js");
|
|
4117
3646
|
|
|
4118
|
-
// raw-ts:/
|
|
3647
|
+
// raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
4119
3648
|
var stripe_setup_default = "import { StripeSync, runMigrations } 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 authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\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";
|
|
4120
3649
|
|
|
4121
|
-
// raw-ts:/
|
|
3650
|
+
// raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
4122
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";
|
|
4123
3652
|
|
|
4124
|
-
// raw-ts:/
|
|
4125
|
-
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 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
|
|
3653
|
+
// raw-ts:/Users/lfdepombo/src/stripe-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 every 10 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";
|
|
4126
3655
|
|
|
4127
3656
|
// src/supabase/edge-function-code.ts
|
|
4128
3657
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -4130,7 +3659,7 @@ var webhookFunctionCode = stripe_webhook_default;
|
|
|
4130
3659
|
var workerFunctionCode = stripe_worker_default;
|
|
4131
3660
|
|
|
4132
3661
|
// src/supabase/supabase.ts
|
|
4133
|
-
var
|
|
3662
|
+
var import_stripe3 = __toESM(require("stripe"), 1);
|
|
4134
3663
|
var STRIPE_SCHEMA_COMMENT_PREFIX = "stripe-sync";
|
|
4135
3664
|
var INSTALLATION_STARTED_SUFFIX = "installation:started";
|
|
4136
3665
|
var INSTALLATION_ERROR_SUFFIX = "installation:error";
|
|
@@ -4390,7 +3919,7 @@ var SupabaseSetupClient = class {
|
|
|
4390
3919
|
* Removes all Edge Functions, secrets, database resources, and Stripe webhooks
|
|
4391
3920
|
*/
|
|
4392
3921
|
async uninstall(stripeSecretKey) {
|
|
4393
|
-
const stripe = new
|
|
3922
|
+
const stripe = new import_stripe3.default(stripeSecretKey, { apiVersion: "2025-02-24.acacia" });
|
|
4394
3923
|
try {
|
|
4395
3924
|
try {
|
|
4396
3925
|
const webhookResult = await this.runSQL(`
|
|
@@ -4536,9 +4065,7 @@ var VALID_SYNC_OBJECTS = [
|
|
|
4536
4065
|
"credit_note",
|
|
4537
4066
|
"early_fraud_warning",
|
|
4538
4067
|
"refund",
|
|
4539
|
-
"checkout_sessions"
|
|
4540
|
-
"subscription_item_change_events_v2_beta",
|
|
4541
|
-
"exchange_rates_from_usd"
|
|
4068
|
+
"checkout_sessions"
|
|
4542
4069
|
];
|
|
4543
4070
|
async function backfillCommand(options, entityName) {
|
|
4544
4071
|
let stripeSync = null;
|
|
@@ -4567,8 +4094,8 @@ async function backfillCommand(options, entityName) {
|
|
|
4567
4094
|
if (!input || input.trim() === "") {
|
|
4568
4095
|
return "Stripe API key is required";
|
|
4569
4096
|
}
|
|
4570
|
-
if (!input.startsWith("sk_")
|
|
4571
|
-
return 'Stripe API key should start with "sk_"
|
|
4097
|
+
if (!input.startsWith("sk_")) {
|
|
4098
|
+
return 'Stripe API key should start with "sk_"';
|
|
4572
4099
|
}
|
|
4573
4100
|
return true;
|
|
4574
4101
|
}
|
|
@@ -4625,7 +4152,6 @@ async function backfillCommand(options, entityName) {
|
|
|
4625
4152
|
stripeSync = new StripeSync({
|
|
4626
4153
|
databaseUrl: config.databaseUrl,
|
|
4627
4154
|
stripeSecretKey: config.stripeApiKey,
|
|
4628
|
-
enableSigmaSync: process.env.ENABLE_SIGMA_SYNC === "true",
|
|
4629
4155
|
stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
|
|
4630
4156
|
autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
|
|
4631
4157
|
backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
|
|
@@ -4789,7 +4315,6 @@ Mode: ${modeLabel}`));
|
|
|
4789
4315
|
stripeSync = new StripeSync({
|
|
4790
4316
|
databaseUrl: config.databaseUrl,
|
|
4791
4317
|
stripeSecretKey: config.stripeApiKey,
|
|
4792
|
-
enableSigmaSync: config.enableSigmaSync,
|
|
4793
4318
|
stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
|
|
4794
4319
|
autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
|
|
4795
4320
|
backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
|
|
@@ -4937,8 +4462,7 @@ async function installCommand(options) {
|
|
|
4937
4462
|
mask: "*",
|
|
4938
4463
|
validate: (input) => {
|
|
4939
4464
|
if (!input.trim()) return "Stripe key is required";
|
|
4940
|
-
if (!input.startsWith("sk_")
|
|
4941
|
-
return 'Stripe key should start with "sk_" or "rk_"';
|
|
4465
|
+
if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
|
|
4942
4466
|
return true;
|
|
4943
4467
|
}
|
|
4944
4468
|
});
|
|
@@ -5008,8 +4532,7 @@ async function uninstallCommand(options) {
|
|
|
5008
4532
|
mask: "*",
|
|
5009
4533
|
validate: (input) => {
|
|
5010
4534
|
if (!input.trim()) return "Stripe key is required";
|
|
5011
|
-
if (!input.startsWith("sk_")
|
|
5012
|
-
return 'Stripe key should start with "sk_" or "rk_"';
|
|
4535
|
+
if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
|
|
5013
4536
|
return true;
|
|
5014
4537
|
}
|
|
5015
4538
|
});
|