stripe-experiment-sync 1.0.8 → 1.0.9-beta.1765909347

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.
@@ -1,16 +1,62 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-CKWVW2JK.js";
3
+ } from "./chunk-3W3CERIG.js";
4
4
 
5
5
  // src/stripeSync.ts
6
- import Stripe2 from "stripe";
6
+ import Stripe3 from "stripe";
7
7
  import { pg as sql2 } from "yesql";
8
8
 
9
9
  // src/database/postgres.ts
10
10
  import pg from "pg";
11
11
  import { pg as sql } from "yesql";
12
+
13
+ // src/database/QueryUtils.ts
14
+ var QueryUtils = class _QueryUtils {
15
+ constructor() {
16
+ }
17
+ static quoteIdent(name) {
18
+ return `"${name}"`;
19
+ }
20
+ static quotedList(names) {
21
+ return names.map(_QueryUtils.quoteIdent).join(", ");
22
+ }
23
+ static buildInsertParts(columns) {
24
+ const columnsSql = columns.map((c) => _QueryUtils.quoteIdent(c.column)).join(", ");
25
+ const valuesSql = columns.map((c, i) => {
26
+ const placeholder = `$${i + 1}`;
27
+ return `${placeholder}::${c.pgType}`;
28
+ }).join(", ");
29
+ const params = columns.map((c) => c.value);
30
+ return { columnsSql, valuesSql, params };
31
+ }
32
+ static buildRawJsonUpsertQuery(schema, table, columns, conflictTarget) {
33
+ const { columnsSql, valuesSql, params } = _QueryUtils.buildInsertParts(columns);
34
+ const conflictSql = _QueryUtils.quotedList(conflictTarget);
35
+ const tsParamIdx = columns.findIndex((c) => c.column === "_last_synced_at") + 1;
36
+ if (tsParamIdx <= 0) {
37
+ throw new Error("buildRawJsonUpsertQuery requires _last_synced_at column");
38
+ }
39
+ const sql3 = `
40
+ INSERT INTO ${_QueryUtils.quoteIdent(schema)}.${_QueryUtils.quoteIdent(table)} (${columnsSql})
41
+ VALUES (${valuesSql})
42
+ ON CONFLICT (${conflictSql})
43
+ DO UPDATE SET
44
+ "_raw_data" = EXCLUDED."_raw_data",
45
+ "_last_synced_at" = $${tsParamIdx},
46
+ "_account_id" = EXCLUDED."_account_id"
47
+ WHERE ${_QueryUtils.quoteIdent(table)}."_last_synced_at" IS NULL
48
+ OR ${_QueryUtils.quoteIdent(table)}."_last_synced_at" < $${tsParamIdx}
49
+ RETURNING *
50
+ `;
51
+ return { sql: sql3, params };
52
+ }
53
+ };
54
+
55
+ // src/database/postgres.ts
12
56
  var ORDERED_STRIPE_TABLES = [
57
+ "exchange_rates_from_usd",
13
58
  "subscription_items",
59
+ "subscription_item_change_events_v2_beta",
14
60
  "subscriptions",
15
61
  "subscription_schedules",
16
62
  "checkout_session_line_items",
@@ -80,7 +126,7 @@ var PostgresClient = class {
80
126
  }
81
127
  return results.flatMap((it) => it.rows);
82
128
  }
83
- async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp) {
129
+ async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp, upsertOptions) {
84
130
  const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
85
131
  if (!entries.length) return [];
86
132
  const chunkSize = 5;
@@ -115,20 +161,33 @@ var PostgresClient = class {
115
161
  const prepared = sql(upsertSql, { useNullForMissing: true })(cleansed);
116
162
  queries.push(this.pool.query(prepared.text, prepared.values));
117
163
  } else {
118
- const rawData = JSON.stringify(entry);
119
- const upsertSql = `
120
- INSERT INTO "${this.config.schema}"."${table}" ("_raw_data", "_last_synced_at", "_account_id")
121
- VALUES ($1::jsonb, $2, $3)
122
- ON CONFLICT (id)
123
- DO UPDATE SET
124
- "_raw_data" = EXCLUDED."_raw_data",
125
- "_last_synced_at" = $2,
126
- "_account_id" = EXCLUDED."_account_id"
127
- WHERE "${table}"."_last_synced_at" IS NULL
128
- OR "${table}"."_last_synced_at" < $2
129
- RETURNING *
130
- `;
131
- queries.push(this.pool.query(upsertSql, [rawData, timestamp, accountId]));
164
+ const conflictTarget = upsertOptions?.conflictTarget ?? ["id"];
165
+ const extraColumns = upsertOptions?.extraColumns ?? [];
166
+ if (!conflictTarget.length) {
167
+ throw new Error(`Invalid upsert config for ${table}: conflictTarget must be non-empty`);
168
+ }
169
+ const columns = [
170
+ { column: "_raw_data", pgType: "jsonb", value: JSON.stringify(entry) },
171
+ ...extraColumns.map((c) => ({
172
+ column: c.column,
173
+ pgType: c.pgType,
174
+ value: entry[c.entryKey]
175
+ })),
176
+ { column: "_last_synced_at", pgType: "timestamptz", value: timestamp },
177
+ { column: "_account_id", pgType: "text", value: accountId }
178
+ ];
179
+ for (const c of columns) {
180
+ if (c.value === void 0) {
181
+ throw new Error(`Missing required value for ${table}.${c.column}`);
182
+ }
183
+ }
184
+ const { sql: upsertSql, params } = QueryUtils.buildRawJsonUpsertQuery(
185
+ this.config.schema,
186
+ table,
187
+ columns,
188
+ conflictTarget
189
+ );
190
+ queries.push(this.pool.query(upsertSql, params));
132
191
  }
133
192
  });
134
193
  results.push(...await Promise.all(queries));
@@ -528,7 +587,12 @@ var PostgresClient = class {
528
587
  } else {
529
588
  await this.query(
530
589
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
531
- SET cursor = $4, updated_at = now()
590
+ SET cursor = CASE
591
+ WHEN cursor IS NULL THEN $4
592
+ WHEN (cursor COLLATE "C") < ($4::text COLLATE "C") THEN $4
593
+ ELSE cursor
594
+ END,
595
+ updated_at = now()
532
596
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
533
597
  [accountId, runStartedAt, object, cursor]
534
598
  );
@@ -539,10 +603,17 @@ var PostgresClient = class {
539
603
  * This considers completed, error, AND running runs to ensure recovery syncs
540
604
  * don't re-process data that was already synced before a crash.
541
605
  * A 'running' status with a cursor means the process was killed mid-sync.
606
+ *
607
+ * Handles two cursor formats:
608
+ * - Numeric: compared as bigint for correct ordering
609
+ * - Composite cursors: compared as strings with COLLATE "C"
542
610
  */
543
611
  async getLastCompletedCursor(accountId, object) {
544
612
  const result = await this.query(
545
- `SELECT MAX(o.cursor::bigint)::text as cursor
613
+ `SELECT CASE
614
+ WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
615
+ ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
616
+ END as cursor
546
617
  FROM "${this.config.schema}"."_sync_obj_runs" o
547
618
  WHERE o."_account_id" = $1
548
619
  AND o.object = $2
@@ -827,6 +898,269 @@ function hashApiKey(apiKey) {
827
898
  return createHash("sha256").update(apiKey).digest("hex");
828
899
  }
829
900
 
901
+ // src/sigma/sigmaApi.ts
902
+ import Papa from "papaparse";
903
+ import Stripe2 from "stripe";
904
+ var STRIPE_FILES_BASE = "https://files.stripe.com/v1";
905
+ function sleep2(ms) {
906
+ return new Promise((resolve) => setTimeout(resolve, ms));
907
+ }
908
+ function parseCsvObjects(csv) {
909
+ const input = csv.replace(/^\uFEFF/, "");
910
+ const parsed = Papa.parse(input, {
911
+ header: true,
912
+ skipEmptyLines: "greedy"
913
+ });
914
+ if (parsed.errors.length > 0) {
915
+ throw new Error(`Failed to parse Sigma CSV: ${parsed.errors[0]?.message ?? "unknown error"}`);
916
+ }
917
+ return parsed.data.filter((row) => row && Object.keys(row).length > 0).map(
918
+ (row) => Object.fromEntries(
919
+ Object.entries(row).map(([k, v]) => [k, v == null || v === "" ? null : String(v)])
920
+ )
921
+ );
922
+ }
923
+ function normalizeSigmaTimestampToIso(value) {
924
+ const v = value.trim();
925
+ if (!v) return null;
926
+ const hasExplicitTz = /z$|[+-]\d{2}:?\d{2}$/i.test(v);
927
+ const isoish = v.includes("T") ? v : v.replace(" ", "T");
928
+ const candidate = hasExplicitTz ? isoish : `${isoish}Z`;
929
+ const d = new Date(candidate);
930
+ if (Number.isNaN(d.getTime())) return null;
931
+ return d.toISOString();
932
+ }
933
+ async function fetchStripeText(url, apiKey, options) {
934
+ const res = await fetch(url, {
935
+ ...options,
936
+ headers: {
937
+ ...options.headers ?? {},
938
+ Authorization: `Bearer ${apiKey}`
939
+ }
940
+ });
941
+ const text = await res.text();
942
+ if (!res.ok) {
943
+ throw new Error(`Sigma file download error (${res.status}) for ${url}: ${text}`);
944
+ }
945
+ return text;
946
+ }
947
+ async function runSigmaQueryAndDownloadCsv(params) {
948
+ const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
949
+ const pollIntervalMs = params.pollIntervalMs ?? 2e3;
950
+ const stripe = new Stripe2(params.apiKey);
951
+ const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
952
+ sql: params.sql
953
+ });
954
+ const queryRunId = created.id;
955
+ const start = Date.now();
956
+ let current = created;
957
+ while (current.status === "running") {
958
+ if (Date.now() - start > pollTimeoutMs) {
959
+ throw new Error(`Sigma query run timed out after ${pollTimeoutMs}ms: ${queryRunId}`);
960
+ }
961
+ await sleep2(pollIntervalMs);
962
+ current = await stripe.rawRequest(
963
+ "GET",
964
+ `/v1/sigma/query_runs/${queryRunId}`,
965
+ {}
966
+ );
967
+ }
968
+ if (current.status !== "succeeded") {
969
+ throw new Error(
970
+ `Sigma query run did not succeed (status=${current.status}) id=${queryRunId} error=${JSON.stringify(
971
+ current.error
972
+ )}`
973
+ );
974
+ }
975
+ const fileId = current.result?.file;
976
+ if (!fileId) {
977
+ throw new Error(`Sigma query run succeeded but result.file is missing (id=${queryRunId})`);
978
+ }
979
+ const csv = await fetchStripeText(
980
+ `${STRIPE_FILES_BASE}/files/${fileId}/contents`,
981
+ params.apiKey,
982
+ { method: "GET" }
983
+ );
984
+ return { queryRunId, fileId, csv };
985
+ }
986
+
987
+ // src/sigma/sigmaIngestionConfigs.ts
988
+ var SIGMA_INGESTION_CONFIGS = {
989
+ subscription_item_change_events_v2_beta: {
990
+ sigmaTable: "subscription_item_change_events_v2_beta",
991
+ destinationTable: "subscription_item_change_events_v2_beta",
992
+ pageSize: 1e4,
993
+ cursor: {
994
+ version: 1,
995
+ columns: [
996
+ { column: "event_timestamp", type: "timestamp" },
997
+ { column: "event_type", type: "string" },
998
+ { column: "subscription_item_id", type: "string" }
999
+ ]
1000
+ },
1001
+ upsert: {
1002
+ conflictTarget: ["_account_id", "event_timestamp", "event_type", "subscription_item_id"],
1003
+ extraColumns: [
1004
+ { column: "event_timestamp", pgType: "timestamptz", entryKey: "event_timestamp" },
1005
+ { column: "event_type", pgType: "text", entryKey: "event_type" },
1006
+ { column: "subscription_item_id", pgType: "text", entryKey: "subscription_item_id" }
1007
+ ]
1008
+ }
1009
+ },
1010
+ exchange_rates_from_usd: {
1011
+ sigmaTable: "exchange_rates_from_usd",
1012
+ destinationTable: "exchange_rates_from_usd",
1013
+ pageSize: 1e4,
1014
+ cursor: {
1015
+ version: 1,
1016
+ columns: [
1017
+ { column: "date", type: "string" },
1018
+ { column: "sell_currency", type: "string" }
1019
+ ]
1020
+ },
1021
+ upsert: {
1022
+ conflictTarget: ["_account_id", "date", "sell_currency"],
1023
+ extraColumns: [
1024
+ { column: "date", pgType: "date", entryKey: "date" },
1025
+ { column: "sell_currency", pgType: "text", entryKey: "sell_currency" }
1026
+ ]
1027
+ }
1028
+ }
1029
+ };
1030
+
1031
+ // src/sigma/sigmaIngestion.ts
1032
+ var SIGMA_CURSOR_DELIM = "";
1033
+ function escapeSigmaSqlStringLiteral(value) {
1034
+ return value.replace(/'/g, "''");
1035
+ }
1036
+ function formatSigmaTimestampForSqlLiteral(date) {
1037
+ return date.toISOString().replace("T", " ").replace("Z", "");
1038
+ }
1039
+ function decodeSigmaCursorValues(spec, cursor) {
1040
+ const prefix = `v${spec.version}${SIGMA_CURSOR_DELIM}`;
1041
+ if (!cursor.startsWith(prefix)) {
1042
+ throw new Error(
1043
+ `Unrecognized Sigma cursor format (expected prefix ${JSON.stringify(prefix)}): ${cursor}`
1044
+ );
1045
+ }
1046
+ const parts = cursor.split(SIGMA_CURSOR_DELIM);
1047
+ const expected = 1 + spec.columns.length;
1048
+ if (parts.length !== expected) {
1049
+ throw new Error(`Malformed Sigma cursor: expected ${expected} parts, got ${parts.length}`);
1050
+ }
1051
+ return parts.slice(1);
1052
+ }
1053
+ function encodeSigmaCursor(spec, values) {
1054
+ if (values.length !== spec.columns.length) {
1055
+ throw new Error(
1056
+ `Cannot encode Sigma cursor: expected ${spec.columns.length} values, got ${values.length}`
1057
+ );
1058
+ }
1059
+ for (const v of values) {
1060
+ if (v.includes(SIGMA_CURSOR_DELIM)) {
1061
+ throw new Error("Cannot encode Sigma cursor: value contains delimiter character");
1062
+ }
1063
+ }
1064
+ return [`v${spec.version}`, ...values].join(SIGMA_CURSOR_DELIM);
1065
+ }
1066
+ function sigmaSqlLiteralForCursorValue(spec, rawValue) {
1067
+ switch (spec.type) {
1068
+ case "timestamp": {
1069
+ const d = new Date(rawValue);
1070
+ if (Number.isNaN(d.getTime())) {
1071
+ throw new Error(`Invalid timestamp cursor value for ${spec.column}: ${rawValue}`);
1072
+ }
1073
+ return `timestamp '${formatSigmaTimestampForSqlLiteral(d)}'`;
1074
+ }
1075
+ case "number": {
1076
+ if (!/^-?\d+(\.\d+)?$/.test(rawValue)) {
1077
+ throw new Error(`Invalid numeric cursor value for ${spec.column}: ${rawValue}`);
1078
+ }
1079
+ return rawValue;
1080
+ }
1081
+ case "string":
1082
+ return `'${escapeSigmaSqlStringLiteral(rawValue)}'`;
1083
+ }
1084
+ }
1085
+ function buildSigmaCursorWhereClause(spec, cursorValues) {
1086
+ if (cursorValues.length !== spec.columns.length) {
1087
+ throw new Error(
1088
+ `Cannot build Sigma cursor predicate: expected ${spec.columns.length} values, got ${cursorValues.length}`
1089
+ );
1090
+ }
1091
+ const cols = spec.columns.map((c) => c.column);
1092
+ const lits = spec.columns.map((c, i) => sigmaSqlLiteralForCursorValue(c, cursorValues[i] ?? ""));
1093
+ const ors = [];
1094
+ for (let i = 0; i < cols.length; i++) {
1095
+ const ands = [];
1096
+ for (let j = 0; j < i; j++) {
1097
+ ands.push(`${cols[j]} = ${lits[j]}`);
1098
+ }
1099
+ ands.push(`${cols[i]} > ${lits[i]}`);
1100
+ ors.push(`(${ands.join(" AND ")})`);
1101
+ }
1102
+ return ors.join(" OR ");
1103
+ }
1104
+ function buildSigmaQuery(config, cursor) {
1105
+ const select = config.select === void 0 || config.select === "*" ? "*" : config.select.join(", ");
1106
+ const whereParts = [];
1107
+ if (config.additionalWhere) {
1108
+ whereParts.push(`(${config.additionalWhere})`);
1109
+ }
1110
+ if (cursor) {
1111
+ const values = decodeSigmaCursorValues(config.cursor, cursor);
1112
+ const predicate = buildSigmaCursorWhereClause(config.cursor, values);
1113
+ whereParts.push(`(${predicate})`);
1114
+ }
1115
+ const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : "";
1116
+ const orderBy = config.cursor.columns.map((c) => c.column).join(", ");
1117
+ return [
1118
+ `SELECT ${select} FROM ${config.sigmaTable}`,
1119
+ whereClause,
1120
+ `ORDER BY ${orderBy} ASC`,
1121
+ `LIMIT ${config.pageSize}`
1122
+ ].filter(Boolean).join(" ");
1123
+ }
1124
+ function defaultSigmaRowToEntry(config, row) {
1125
+ const out = { ...row };
1126
+ for (const col of config.cursor.columns) {
1127
+ const raw = row[col.column];
1128
+ if (raw == null) {
1129
+ throw new Error(`Sigma row missing required cursor column: ${col.column}`);
1130
+ }
1131
+ if (col.type === "timestamp") {
1132
+ const normalized = normalizeSigmaTimestampToIso(raw);
1133
+ if (!normalized) {
1134
+ throw new Error(`Sigma row has invalid timestamp for ${col.column}: ${raw}`);
1135
+ }
1136
+ out[col.column] = normalized;
1137
+ } else if (col.type === "string") {
1138
+ const v = raw.trim();
1139
+ if (!v) {
1140
+ throw new Error(`Sigma row has empty string for required cursor column: ${col.column}`);
1141
+ }
1142
+ out[col.column] = v;
1143
+ } else {
1144
+ const v = raw.trim();
1145
+ if (!v) {
1146
+ throw new Error(`Sigma row has empty value for required cursor column: ${col.column}`);
1147
+ }
1148
+ out[col.column] = v;
1149
+ }
1150
+ }
1151
+ return out;
1152
+ }
1153
+ function sigmaCursorFromEntry(config, entry) {
1154
+ const values = config.cursor.columns.map((c) => {
1155
+ const raw = entry[c.column];
1156
+ if (raw == null) {
1157
+ throw new Error(`Cannot build cursor: entry missing ${c.column}`);
1158
+ }
1159
+ return String(raw);
1160
+ });
1161
+ return encodeSigmaCursor(config.cursor, values);
1162
+ }
1163
+
830
1164
  // src/stripeSync.ts
831
1165
  function getUniqueIds(entries, key) {
832
1166
  const set = new Set(
@@ -837,7 +1171,7 @@ function getUniqueIds(entries, key) {
837
1171
  var StripeSync = class {
838
1172
  constructor(config) {
839
1173
  this.config = config;
840
- const baseStripe = new Stripe2(config.stripeSecretKey, {
1174
+ const baseStripe = new Stripe3(config.stripeSecretKey, {
841
1175
  // https://github.com/stripe/stripe-node#configuration
842
1176
  // @ts-ignore
843
1177
  apiVersion: config.stripeApiVersion,
@@ -1238,6 +1572,17 @@ var StripeSync = class {
1238
1572
  listFn: (p) => this.stripe.checkout.sessions.list(p),
1239
1573
  upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
1240
1574
  supportsCreatedFilter: true
1575
+ },
1576
+ // Sigma-backed resources
1577
+ subscription_item_change_events_v2_beta: {
1578
+ order: 18,
1579
+ supportsCreatedFilter: false,
1580
+ sigma: SIGMA_INGESTION_CONFIGS.subscription_item_change_events_v2_beta
1581
+ },
1582
+ exchange_rates_from_usd: {
1583
+ order: 19,
1584
+ supportsCreatedFilter: false,
1585
+ sigma: SIGMA_INGESTION_CONFIGS.exchange_rates_from_usd
1241
1586
  }
1242
1587
  };
1243
1588
  async processEvent(event) {
@@ -1270,7 +1615,13 @@ var StripeSync = class {
1270
1615
  * Order is determined by the `order` field in resourceRegistry.
1271
1616
  */
1272
1617
  getSupportedSyncObjects() {
1273
- return Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1618
+ const all = Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1619
+ if (!this.config.enableSigmaSync) {
1620
+ return all.filter(
1621
+ (o) => o !== "subscription_item_change_events_v2_beta" && o !== "exchange_rates_from_usd"
1622
+ );
1623
+ }
1624
+ return all;
1274
1625
  }
1275
1626
  // Event handler methods
1276
1627
  async handleChargeEvent(event, accountId) {
@@ -1349,7 +1700,7 @@ var StripeSync = class {
1349
1700
  );
1350
1701
  await this.upsertProducts([product], accountId, this.getSyncTimestamp(event, refetched));
1351
1702
  } catch (err) {
1352
- if (err instanceof Stripe2.errors.StripeAPIError && err.code === "resource_missing") {
1703
+ if (err instanceof Stripe3.errors.StripeAPIError && err.code === "resource_missing") {
1353
1704
  const product = event.data.object;
1354
1705
  await this.deleteProduct(product.id);
1355
1706
  } else {
@@ -1369,7 +1720,7 @@ var StripeSync = class {
1369
1720
  );
1370
1721
  await this.upsertPrices([price], accountId, false, this.getSyncTimestamp(event, refetched));
1371
1722
  } catch (err) {
1372
- if (err instanceof Stripe2.errors.StripeAPIError && err.code === "resource_missing") {
1723
+ if (err instanceof Stripe3.errors.StripeAPIError && err.code === "resource_missing") {
1373
1724
  const price = event.data.object;
1374
1725
  await this.deletePrice(price.id);
1375
1726
  } else {
@@ -1389,7 +1740,7 @@ var StripeSync = class {
1389
1740
  );
1390
1741
  await this.upsertPlans([plan], accountId, false, this.getSyncTimestamp(event, refetched));
1391
1742
  } catch (err) {
1392
- if (err instanceof Stripe2.errors.StripeAPIError && err.code === "resource_missing") {
1743
+ if (err instanceof Stripe3.errors.StripeAPIError && err.code === "resource_missing") {
1393
1744
  const plan = event.data.object;
1394
1745
  await this.deletePlan(plan.id);
1395
1746
  } else {
@@ -1636,10 +1987,10 @@ var StripeSync = class {
1636
1987
  let cursor = null;
1637
1988
  if (!params?.created) {
1638
1989
  if (objRun?.cursor) {
1639
- cursor = parseInt(objRun.cursor);
1990
+ cursor = objRun.cursor;
1640
1991
  } else {
1641
1992
  const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
1642
- cursor = lastCursor ? parseInt(lastCursor) : null;
1993
+ cursor = lastCursor ?? null;
1643
1994
  }
1644
1995
  }
1645
1996
  const result = await this.fetchOnePage(
@@ -1694,9 +2045,18 @@ var StripeSync = class {
1694
2045
  throw new Error(`Unsupported object type for processNext: ${object}`);
1695
2046
  }
1696
2047
  try {
2048
+ if (config.sigma) {
2049
+ return await this.fetchOneSigmaPage(
2050
+ accountId,
2051
+ resourceName,
2052
+ runStartedAt,
2053
+ cursor,
2054
+ config.sigma
2055
+ );
2056
+ }
1697
2057
  const listParams = { limit };
1698
2058
  if (config.supportsCreatedFilter) {
1699
- const created = params?.created ?? (cursor ? { gte: cursor } : void 0);
2059
+ const created = params?.created ?? (cursor && /^\d+$/.test(cursor) ? { gte: Number.parseInt(cursor, 10) } : void 0);
1700
2060
  if (created) {
1701
2061
  listParams.created = created;
1702
2062
  }
@@ -1741,6 +2101,97 @@ var StripeSync = class {
1741
2101
  throw error;
1742
2102
  }
1743
2103
  }
2104
+ async getSigmaFallbackCursorFromDestination(accountId, sigmaConfig) {
2105
+ const cursorCols = sigmaConfig.cursor.columns;
2106
+ const selectCols = cursorCols.map((c) => `"${c.column}"`).join(", ");
2107
+ const orderBy = cursorCols.map((c) => `"${c.column}" DESC`).join(", ");
2108
+ const result = await this.postgresClient.query(
2109
+ `SELECT ${selectCols}
2110
+ FROM "stripe"."${sigmaConfig.destinationTable}"
2111
+ WHERE "_account_id" = $1
2112
+ ORDER BY ${orderBy}
2113
+ LIMIT 1`,
2114
+ [accountId]
2115
+ );
2116
+ if (result.rows.length === 0) return null;
2117
+ const row = result.rows[0];
2118
+ const entryForCursor = {};
2119
+ for (const c of cursorCols) {
2120
+ const v = row[c.column];
2121
+ if (v == null) {
2122
+ throw new Error(
2123
+ `Sigma fallback cursor query returned null for ${sigmaConfig.destinationTable}.${c.column}`
2124
+ );
2125
+ }
2126
+ if (c.type === "timestamp") {
2127
+ const d = v instanceof Date ? v : new Date(String(v));
2128
+ if (Number.isNaN(d.getTime())) {
2129
+ throw new Error(
2130
+ `Sigma fallback cursor query returned invalid timestamp for ${sigmaConfig.destinationTable}.${c.column}: ${String(
2131
+ v
2132
+ )}`
2133
+ );
2134
+ }
2135
+ entryForCursor[c.column] = d.toISOString();
2136
+ } else {
2137
+ entryForCursor[c.column] = String(v);
2138
+ }
2139
+ }
2140
+ return sigmaCursorFromEntry(sigmaConfig, entryForCursor);
2141
+ }
2142
+ async fetchOneSigmaPage(accountId, resourceName, runStartedAt, cursor, sigmaConfig) {
2143
+ if (!this.config.stripeSecretKey) {
2144
+ throw new Error("Sigma sync requested but stripeSecretKey is not configured.");
2145
+ }
2146
+ if (resourceName !== sigmaConfig.destinationTable) {
2147
+ throw new Error(
2148
+ `Sigma sync config mismatch: resourceName=${resourceName} destinationTable=${sigmaConfig.destinationTable}`
2149
+ );
2150
+ }
2151
+ const effectiveCursor = cursor ?? await this.getSigmaFallbackCursorFromDestination(accountId, sigmaConfig);
2152
+ const sigmaSql = buildSigmaQuery(sigmaConfig, effectiveCursor);
2153
+ this.config.logger?.info(
2154
+ { object: resourceName, pageSize: sigmaConfig.pageSize, hasCursor: Boolean(effectiveCursor) },
2155
+ "Sigma sync: running query"
2156
+ );
2157
+ const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
2158
+ apiKey: this.config.stripeSecretKey,
2159
+ sql: sigmaSql,
2160
+ logger: this.config.logger
2161
+ });
2162
+ const rows = parseCsvObjects(csv);
2163
+ if (rows.length === 0) {
2164
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2165
+ return { processed: 0, hasMore: false, runStartedAt };
2166
+ }
2167
+ const entries = rows.map(
2168
+ (row) => defaultSigmaRowToEntry(sigmaConfig, row)
2169
+ );
2170
+ this.config.logger?.info(
2171
+ { object: resourceName, rows: entries.length, queryRunId, fileId },
2172
+ "Sigma sync: upserting rows"
2173
+ );
2174
+ await this.postgresClient.upsertManyWithTimestampProtection(
2175
+ entries,
2176
+ resourceName,
2177
+ accountId,
2178
+ void 0,
2179
+ sigmaConfig.upsert
2180
+ );
2181
+ await this.postgresClient.incrementObjectProgress(
2182
+ accountId,
2183
+ runStartedAt,
2184
+ resourceName,
2185
+ entries.length
2186
+ );
2187
+ const newCursor = sigmaCursorFromEntry(sigmaConfig, entries[entries.length - 1]);
2188
+ await this.postgresClient.updateObjectCursor(accountId, runStartedAt, resourceName, newCursor);
2189
+ const hasMore = rows.length === sigmaConfig.pageSize;
2190
+ if (!hasMore) {
2191
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2192
+ }
2193
+ return { processed: entries.length, hasMore, runStartedAt };
2194
+ }
1744
2195
  /**
1745
2196
  * Process all pages for all (or specified) object types until complete.
1746
2197
  *
@@ -1869,6 +2320,12 @@ var StripeSync = class {
1869
2320
  case "checkout_sessions":
1870
2321
  results.checkoutSessions = result;
1871
2322
  break;
2323
+ case "subscription_item_change_events_v2_beta":
2324
+ results.subscriptionItemChangeEventsV2Beta = result;
2325
+ break;
2326
+ case "exchange_rates_from_usd":
2327
+ results.exchangeRatesFromUsd = result;
2328
+ break;
1872
2329
  }
1873
2330
  }
1874
2331
  }
@@ -3429,6 +3886,8 @@ var VERSION = package_default.version;
3429
3886
  export {
3430
3887
  PostgresClient,
3431
3888
  hashApiKey,
3889
+ parseCsvObjects,
3890
+ normalizeSigmaTimestampToIso,
3432
3891
  StripeSync,
3433
3892
  runMigrations,
3434
3893
  createStripeWebSocketClient,
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "stripe-experiment-sync",
4
- version: "1.0.8-beta.1765856228",
4
+ version: "1.0.9-beta.1765909347",
5
5
  private: false,
6
6
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
7
  type: "module",
@@ -41,6 +41,7 @@ var package_default = {
41
41
  dotenv: "^16.4.7",
42
42
  express: "^4.18.2",
43
43
  inquirer: "^12.3.0",
44
+ papaparse: "5.4.1",
44
45
  pg: "^8.16.3",
45
46
  "pg-node-migrations": "0.0.8",
46
47
  stripe: "^17.7.0",
@@ -52,6 +53,7 @@ var package_default = {
52
53
  "@types/express": "^4.17.21",
53
54
  "@types/inquirer": "^9.0.7",
54
55
  "@types/node": "^24.10.1",
56
+ "@types/papaparse": "5.3.16",
55
57
  "@types/pg": "^8.15.5",
56
58
  "@types/ws": "^8.5.13",
57
59
  "@types/yesql": "^4.1.4",